From 0d0d7b250423a0c339330269ef99c433241def2a Mon Sep 17 00:00:00 2001 From: tonytrg Date: Thu, 18 Sep 2025 16:21:06 +0200 Subject: [PATCH 01/12] poc gql tool create label --- pkg/github/issues.go | 124 +++++++++++++++++++++++++++++++++++++++++++ pkg/github/tools.go | 1 + 2 files changed, 125 insertions(+) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 1c88a9fde..d11d78aee 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -1700,6 +1700,130 @@ func AssignCopilotToIssue(getGQLClient GetGQLClientFn, t translations.Translatio } } +// Label Management + +// Create label +func CreateLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { + return mcp.NewTool("create_label", + mcp.WithDescription(t("TOOL_CREATE_LABEL_DESCRIPTION", "Create a new label in a GitHub repository.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_CREATE_LABEL_TITLE", "Create label"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithString("name", + mcp.Required(), + mcp.Description("Name of the label to create"), + ), + mcp.WithString("color", + mcp.Required(), + mcp.Description("Label color as a 6-character hex code without '#', e.g. 'f29513'"), + ), + mcp.WithString("description", + mcp.Description("Label description"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + name, err := RequiredParam[string](request, "name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + color, err := OptionalParam[string](request, "color") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + description, err := OptionalParam[string](request, "description") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getGQLClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // First fetch the repository node ID since createLabel requires a repositoryId + var repoQuery struct { + Repository struct { + ID githubv4.ID + } `graphql:"repository(owner: $owner, name: $repo)"` + } + + vars := map[string]any{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + } + + if err := client.Query(ctx, &repoQuery, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find repository", err), nil + } + + // Build the input for createLabel. Only set optional fields when provided. + input := githubv4.CreateLabelInput{ + RepositoryID: repoQuery.Repository.ID, + Name: githubv4.String(name), + } + if color != "" { + input.Color = githubv4.String(color) + } + if description != "" { + d := githubv4.String(description) + input.Description = &d + } + + var mutation struct { + CreateLabel struct { + Label struct { + ID githubv4.ID + Name githubv4.String + Color githubv4.String + Description githubv4.String + } + } `graphql:"createLabel(input: $input)"` + } + + if err := client.Mutate(ctx, &mutation, input, nil); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to create label", err), nil + } + + out := map[string]any{ + "id": fmt.Sprintf("%v", mutation.CreateLabel.Label.ID), + "name": string(mutation.CreateLabel.Label.Name), + "color": string(mutation.CreateLabel.Label.Color), + "description": string(mutation.CreateLabel.Label.Description), + } + + r, err := json.Marshal(out) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// Get label +//func GetLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {} +// Update label +//func UpdateLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {} +// Delete label +//func DeleteLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {} + type ReplaceActorsForAssignableInput struct { AssignableID githubv4.ID `json:"assignableId"` ActorIDs []githubv4.ID `json:"actorIds"` diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 0f294cef6..b741a0c63 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -70,6 +70,7 @@ 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(CreateLabel(getGQLClient, t)), ).AddPrompts( toolsets.NewServerPrompt(AssignCodingAgentPrompt(t)), toolsets.NewServerPrompt(IssueToFixWorkflowPrompt(t)), From 1a8722f6e3969b7b6abf0538ebf71aee69811c8b Mon Sep 17 00:00:00 2001 From: tonytrg Date: Fri, 19 Sep 2025 16:14:37 +0200 Subject: [PATCH 02/12] adding placeholder test functions --- pkg/github/issues.go | 338 ++++++++++++++++++++++++++++++++++++------- pkg/github/tools.go | 3 + 2 files changed, 286 insertions(+), 55 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index d11d78aee..a50f69a2c 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -1789,10 +1789,8 @@ func CreateLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFu var mutation struct { CreateLabel struct { Label struct { - ID githubv4.ID - Name githubv4.String - Color githubv4.String - Description githubv4.String + Name githubv4.String + ID githubv4.ID } } `graphql:"createLabel(input: $input)"` } @@ -1801,29 +1799,298 @@ func CreateLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFu return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to create label", err), nil } - out := map[string]any{ - "id": fmt.Sprintf("%v", mutation.CreateLabel.Label.ID), - "name": string(mutation.CreateLabel.Label.Name), - "color": string(mutation.CreateLabel.Label.Color), - "description": string(mutation.CreateLabel.Label.Description), + return mcp.NewToolResultText(fmt.Sprintf("label %s created successfully", mutation.CreateLabel.Label.Name)), nil + } +} + +// Get label +func GetLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { + return mcp.NewTool("get_label", + mcp.WithDescription(t("TOOL_GET_LABEL_DESCRIPTION", "Get a label in a GitHub repository.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_GET_LABEL_TITLE", "Get label"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithString("name", + mcp.Required(), + mcp.Description("Name of the label to retrieve"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + name, err := RequiredParam[string](request, "name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil } - r, err := json.Marshal(out) + client, err := getGQLClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf("failed to get GitHub client: %w", err) } - return mcp.NewToolResultText(string(r)), nil + var query struct { + Repository struct { + Label struct { + ID githubv4.ID + Name githubv4.String + Color githubv4.String + Description githubv4.String + } `graphql:"label(name: $name)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } + + vars := map[string]any{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + "name": githubv4.String(name), + } + + if err := client.Query(ctx, &query, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", err), nil + } + + // If label wasn't found, return a helpful error + if query.Repository.Label.Name == "" { + return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil + } + + label := map[string]interface{}{ + "id": fmt.Sprintf("%v", query.Repository.Label.ID), + "name": string(query.Repository.Label.Name), + "color": string(query.Repository.Label.Color), + "description": string(query.Repository.Label.Description), + } + + out, err := json.Marshal(label) + if err != nil { + return nil, fmt.Errorf("failed to marshal label: %w", err) + } + + return mcp.NewToolResultText(string(out)), nil } } -// Get label -//func GetLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {} // Update label -//func UpdateLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {} +func UpdateLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { + return mcp.NewTool("update_label", + mcp.WithDescription(t("TOOL_UPDATE_LABEL_DESCRIPTION", "Update an existing label in a GitHub repository.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_UPDATE_LABEL_TITLE", "Update label"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithString("name", + mcp.Required(), + mcp.Description("Name of the existing label to update"), + ), + mcp.WithString("new_name", + mcp.Description("New name for the label"), + ), + mcp.WithString("color", + mcp.Description("New label color as a 6-character hex code without '#', e.g. 'f29513'"), + ), + mcp.WithString("description", + mcp.Description("New label description"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + name, err := RequiredParam[string](request, "name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + newName, err := OptionalParam[string](request, "new_name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + color, err := OptionalParam[string](request, "color") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + description, err := OptionalParam[string](request, "description") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getGQLClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Fetch the label to get its GQL ID + var query struct { + Repository struct { + Label struct { + ID githubv4.ID + Name githubv4.String + } `graphql:"label(name: $name)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } + + vars := map[string]any{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + "name": githubv4.String(name), + } + + if err := client.Query(ctx, &query, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", err), nil + } + + // If label wasn't found, return a helpful error + if query.Repository.Label.Name == "" { + return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil + } + + // Build the update input, only set fields that were provided + input := githubv4.UpdateLabelInput{ + ID: query.Repository.Label.ID, + } + 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 + } + + return mcp.NewToolResultText(fmt.Sprintf("label %s updated successfully", mutation.UpdateLabel.Label.Name)), nil + } +} + // Delete label -//func DeleteLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {} +func DeleteLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { + return mcp.NewTool("delete_label", + mcp.WithDescription(t("TOOL_DELETE_LABEL_DESCRIPTION", "Delete an existing label from a GitHub repository.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_DELETE_LABEL_TITLE", "Delete label"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithString("name", + mcp.Required(), + mcp.Description("Name of the label to delete"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + name, err := RequiredParam[string](request, "name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + client, err := getGQLClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Fetch the label to get its GQL ID + var query struct { + Repository struct { + Label struct { + ID githubv4.ID + Name githubv4.String + } `graphql:"label(name: $name)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } + + vars := map[string]any{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + "name": githubv4.String(name), + } + + if err := client.Query(ctx, &query, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", err), nil + } + + // If label wasn't found, return a helpful error + if query.Repository.Label.Name == "" { + return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil + } + + input := githubv4.DeleteLabelInput{ + ID: query.Repository.Label.ID, + } + + var mutation struct { + DeleteLabel struct { + Typename githubv4.String `graphql:"__typename"` + } `graphql:"deleteLabel(input: $input)"` + } + + if err := client.Mutate(ctx, &mutation, input, nil); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to delete label", err), nil + } + + return mcp.NewToolResultText(fmt.Sprintf("label %s deleted successfully", name)), nil + } +} + +// ReplaceActorsForAssignableInput represents the input for replacing actors for an assignable entity. +// Used in the AssignCopilotToIssue tool to assign Copilot to an issue. type ReplaceActorsForAssignableInput struct { AssignableID githubv4.ID `json:"assignableId"` ActorIDs []githubv4.ID `json:"actorIds"` @@ -1852,42 +2119,3 @@ func parseISOTimestamp(timestamp string) (time.Time, error) { // Return error with supported formats return time.Time{}, fmt.Errorf("invalid ISO 8601 timestamp: %s (supported formats: YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DD)", timestamp) } - -func AssignCodingAgentPrompt(t translations.TranslationHelperFunc) (tool mcp.Prompt, handler server.PromptHandlerFunc) { - return mcp.NewPrompt("AssignCodingAgent", - mcp.WithPromptDescription(t("PROMPT_ASSIGN_CODING_AGENT_DESCRIPTION", "Assign GitHub Coding Agent to multiple tasks in a GitHub repository.")), - mcp.WithArgument("repo", mcp.ArgumentDescription("The repository to assign tasks in (owner/repo)."), mcp.RequiredArgument()), - ), func(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { - repo := request.Params.Arguments["repo"] - - messages := []mcp.PromptMessage{ - { - Role: "user", - Content: mcp.NewTextContent("You are a personal assistant for GitHub the Copilot GitHub Coding Agent. Your task is to help the user assign tasks to the Coding Agent based on their open GitHub issues. You can use `assign_copilot_to_issue` tool to assign the Coding Agent to issues that are suitable for autonomous work, and `search_issues` tool to find issues that match the user's criteria. You can also use `list_issues` to get a list of issues in the repository."), - }, - { - Role: "user", - Content: mcp.NewTextContent(fmt.Sprintf("Please go and get a list of the most recent 10 issues from the %s GitHub repository", repo)), - }, - { - Role: "assistant", - Content: mcp.NewTextContent(fmt.Sprintf("Sure! I will get a list of the 10 most recent issues for the repo %s.", repo)), - }, - { - Role: "user", - Content: mcp.NewTextContent("For each issue, please check if it is a clearly defined coding task with acceptance criteria and a low to medium complexity to identify issues that are suitable for an AI Coding Agent to work on. Then assign each of the identified issues to Copilot."), - }, - { - Role: "assistant", - Content: mcp.NewTextContent("Certainly! Let me carefully check which ones are clearly scoped issues that are good to assign to the coding agent, and I will summarize and assign them now."), - }, - { - Role: "user", - Content: mcp.NewTextContent("Great, if you are unsure if an issue is good to assign, ask me first, rather than assigning copilot. If you are certain the issue is clear and suitable you can assign it to Copilot without asking."), - }, - } - return &mcp.GetPromptResult{ - Messages: messages, - }, nil - } -} diff --git a/pkg/github/tools.go b/pkg/github/tools.go index b741a0c63..af4fcce96 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -61,6 +61,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(GetIssueComments(getClient, t)), toolsets.NewServerTool(ListIssueTypes(getClient, t)), toolsets.NewServerTool(ListSubIssues(getClient, t)), + toolsets.NewServerTool(GetLabel(getGQLClient, t)), ). AddWriteTools( toolsets.NewServerTool(CreateIssue(getClient, t)), @@ -71,6 +72,8 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(RemoveSubIssue(getClient, t)), toolsets.NewServerTool(ReprioritizeSubIssue(getClient, t)), toolsets.NewServerTool(CreateLabel(getGQLClient, t)), + toolsets.NewServerTool(UpdateLabel(getGQLClient, t)), + toolsets.NewServerTool(DeleteLabel(getGQLClient, t)), ).AddPrompts( toolsets.NewServerPrompt(AssignCodingAgentPrompt(t)), toolsets.NewServerPrompt(IssueToFixWorkflowPrompt(t)), From 3c325592f3f562f1b2113cc7404efb738eb7a45d Mon Sep 17 00:00:00 2001 From: tonytrg Date: Fri, 19 Sep 2025 16:17:01 +0200 Subject: [PATCH 03/12] readd tool --- pkg/github/issues.go | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index a50f69a2c..10ff5a460 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -2096,6 +2096,45 @@ type ReplaceActorsForAssignableInput struct { ActorIDs []githubv4.ID `json:"actorIds"` } +func AssignCodingAgentPrompt(t translations.TranslationHelperFunc) (tool mcp.Prompt, handler server.PromptHandlerFunc) { + return mcp.NewPrompt("AssignCodingAgent", + mcp.WithPromptDescription(t("PROMPT_ASSIGN_CODING_AGENT_DESCRIPTION", "Assign GitHub Coding Agent to multiple tasks in a GitHub repository.")), + mcp.WithArgument("repo", mcp.ArgumentDescription("The repository to assign tasks in (owner/repo)."), mcp.RequiredArgument()), + ), func(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + repo := request.Params.Arguments["repo"] + + messages := []mcp.PromptMessage{ + { + Role: "user", + Content: mcp.NewTextContent("You are a personal assistant for GitHub the Copilot GitHub Coding Agent. Your task is to help the user assign tasks to the Coding Agent based on their open GitHub issues. You can use `assign_copilot_to_issue` tool to assign the Coding Agent to issues that are suitable for autonomous work, and `search_issues` tool to find issues that match the user's criteria. You can also use `list_issues` to get a list of issues in the repository."), + }, + { + Role: "user", + Content: mcp.NewTextContent(fmt.Sprintf("Please go and get a list of the most recent 10 issues from the %s GitHub repository", repo)), + }, + { + Role: "assistant", + Content: mcp.NewTextContent(fmt.Sprintf("Sure! I will get a list of the 10 most recent issues for the repo %s.", repo)), + }, + { + Role: "user", + Content: mcp.NewTextContent("For each issue, please check if it is a clearly defined coding task with acceptance criteria and a low to medium complexity to identify issues that are suitable for an AI Coding Agent to work on. Then assign each of the identified issues to Copilot."), + }, + { + Role: "assistant", + Content: mcp.NewTextContent("Certainly! Let me carefully check which ones are clearly scoped issues that are good to assign to the coding agent, and I will summarize and assign them now."), + }, + { + Role: "user", + Content: mcp.NewTextContent("Great, if you are unsure if an issue is good to assign, ask me first, rather than assigning copilot. If you are certain the issue is clear and suitable you can assign it to Copilot without asking."), + }, + } + return &mcp.GetPromptResult{ + Messages: messages, + }, nil + } +} + // parseISOTimestamp parses an ISO 8601 timestamp string into a time.Time object. // Returns the parsed time or an error if parsing fails. // Example formats supported: "2023-01-15T14:30:00Z", "2023-01-15" From 80a323780262d92d93a50f2f47839c34f41b5103 Mon Sep 17 00:00:00 2001 From: tonytrg Date: Fri, 19 Sep 2025 16:19:02 +0200 Subject: [PATCH 04/12] move label tools --- pkg/github/issues.go | 138 +++++++++++++++++++++---------------------- 1 file changed, 68 insertions(+), 70 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 10ff5a460..42f0a1bf5 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -1700,6 +1700,74 @@ func AssignCopilotToIssue(getGQLClient GetGQLClientFn, t translations.Translatio } } +type ReplaceActorsForAssignableInput struct { + AssignableID githubv4.ID `json:"assignableId"` + ActorIDs []githubv4.ID `json:"actorIds"` +} + +// parseISOTimestamp parses an ISO 8601 timestamp string into a time.Time object. +// Returns the parsed time or an error if parsing fails. +// Example formats supported: "2023-01-15T14:30:00Z", "2023-01-15" +func parseISOTimestamp(timestamp string) (time.Time, error) { + if timestamp == "" { + return time.Time{}, fmt.Errorf("empty timestamp") + } + + // Try RFC3339 format (standard ISO 8601 with time) + t, err := time.Parse(time.RFC3339, timestamp) + if err == nil { + return t, nil + } + + // Try simple date format (YYYY-MM-DD) + t, err = time.Parse("2006-01-02", timestamp) + if err == nil { + return t, nil + } + + // Return error with supported formats + return time.Time{}, fmt.Errorf("invalid ISO 8601 timestamp: %s (supported formats: YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DD)", timestamp) +} + +func AssignCodingAgentPrompt(t translations.TranslationHelperFunc) (tool mcp.Prompt, handler server.PromptHandlerFunc) { + return mcp.NewPrompt("AssignCodingAgent", + mcp.WithPromptDescription(t("PROMPT_ASSIGN_CODING_AGENT_DESCRIPTION", "Assign GitHub Coding Agent to multiple tasks in a GitHub repository.")), + mcp.WithArgument("repo", mcp.ArgumentDescription("The repository to assign tasks in (owner/repo)."), mcp.RequiredArgument()), + ), func(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + repo := request.Params.Arguments["repo"] + + messages := []mcp.PromptMessage{ + { + Role: "user", + Content: mcp.NewTextContent("You are a personal assistant for GitHub the Copilot GitHub Coding Agent. Your task is to help the user assign tasks to the Coding Agent based on their open GitHub issues. You can use `assign_copilot_to_issue` tool to assign the Coding Agent to issues that are suitable for autonomous work, and `search_issues` tool to find issues that match the user's criteria. You can also use `list_issues` to get a list of issues in the repository."), + }, + { + Role: "user", + Content: mcp.NewTextContent(fmt.Sprintf("Please go and get a list of the most recent 10 issues from the %s GitHub repository", repo)), + }, + { + Role: "assistant", + Content: mcp.NewTextContent(fmt.Sprintf("Sure! I will get a list of the 10 most recent issues for the repo %s.", repo)), + }, + { + Role: "user", + Content: mcp.NewTextContent("For each issue, please check if it is a clearly defined coding task with acceptance criteria and a low to medium complexity to identify issues that are suitable for an AI Coding Agent to work on. Then assign each of the identified issues to Copilot."), + }, + { + Role: "assistant", + Content: mcp.NewTextContent("Certainly! Let me carefully check which ones are clearly scoped issues that are good to assign to the coding agent, and I will summarize and assign them now."), + }, + { + Role: "user", + Content: mcp.NewTextContent("Great, if you are unsure if an issue is good to assign, ask me first, rather than assigning copilot. If you are certain the issue is clear and suitable you can assign it to Copilot without asking."), + }, + } + return &mcp.GetPromptResult{ + Messages: messages, + }, nil + } +} + // Label Management // Create label @@ -2088,73 +2156,3 @@ func DeleteLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFu return mcp.NewToolResultText(fmt.Sprintf("label %s deleted successfully", name)), nil } } - -// ReplaceActorsForAssignableInput represents the input for replacing actors for an assignable entity. -// Used in the AssignCopilotToIssue tool to assign Copilot to an issue. -type ReplaceActorsForAssignableInput struct { - AssignableID githubv4.ID `json:"assignableId"` - ActorIDs []githubv4.ID `json:"actorIds"` -} - -func AssignCodingAgentPrompt(t translations.TranslationHelperFunc) (tool mcp.Prompt, handler server.PromptHandlerFunc) { - return mcp.NewPrompt("AssignCodingAgent", - mcp.WithPromptDescription(t("PROMPT_ASSIGN_CODING_AGENT_DESCRIPTION", "Assign GitHub Coding Agent to multiple tasks in a GitHub repository.")), - mcp.WithArgument("repo", mcp.ArgumentDescription("The repository to assign tasks in (owner/repo)."), mcp.RequiredArgument()), - ), func(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { - repo := request.Params.Arguments["repo"] - - messages := []mcp.PromptMessage{ - { - Role: "user", - Content: mcp.NewTextContent("You are a personal assistant for GitHub the Copilot GitHub Coding Agent. Your task is to help the user assign tasks to the Coding Agent based on their open GitHub issues. You can use `assign_copilot_to_issue` tool to assign the Coding Agent to issues that are suitable for autonomous work, and `search_issues` tool to find issues that match the user's criteria. You can also use `list_issues` to get a list of issues in the repository."), - }, - { - Role: "user", - Content: mcp.NewTextContent(fmt.Sprintf("Please go and get a list of the most recent 10 issues from the %s GitHub repository", repo)), - }, - { - Role: "assistant", - Content: mcp.NewTextContent(fmt.Sprintf("Sure! I will get a list of the 10 most recent issues for the repo %s.", repo)), - }, - { - Role: "user", - Content: mcp.NewTextContent("For each issue, please check if it is a clearly defined coding task with acceptance criteria and a low to medium complexity to identify issues that are suitable for an AI Coding Agent to work on. Then assign each of the identified issues to Copilot."), - }, - { - Role: "assistant", - Content: mcp.NewTextContent("Certainly! Let me carefully check which ones are clearly scoped issues that are good to assign to the coding agent, and I will summarize and assign them now."), - }, - { - Role: "user", - Content: mcp.NewTextContent("Great, if you are unsure if an issue is good to assign, ask me first, rather than assigning copilot. If you are certain the issue is clear and suitable you can assign it to Copilot without asking."), - }, - } - return &mcp.GetPromptResult{ - Messages: messages, - }, nil - } -} - -// parseISOTimestamp parses an ISO 8601 timestamp string into a time.Time object. -// Returns the parsed time or an error if parsing fails. -// Example formats supported: "2023-01-15T14:30:00Z", "2023-01-15" -func parseISOTimestamp(timestamp string) (time.Time, error) { - if timestamp == "" { - return time.Time{}, fmt.Errorf("empty timestamp") - } - - // Try RFC3339 format (standard ISO 8601 with time) - t, err := time.Parse(time.RFC3339, timestamp) - if err == nil { - return t, nil - } - - // Try simple date format (YYYY-MM-DD) - t, err = time.Parse("2006-01-02", timestamp) - if err == nil { - return t, nil - } - - // Return error with supported formats - return time.Time{}, fmt.Errorf("invalid ISO 8601 timestamp: %s (supported formats: YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DD)", timestamp) -} From 01122f820d51b63578672f3d9094e6f85c8e3898 Mon Sep 17 00:00:00 2001 From: tonytrg Date: Fri, 19 Sep 2025 17:09:10 +0200 Subject: [PATCH 05/12] add crud tools for labels --- pkg/github/issues.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 42f0a1bf5..b11fb6289 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -1770,10 +1770,10 @@ func AssignCodingAgentPrompt(t translations.TranslationHelperFunc) (tool mcp.Pro // Label Management -// Create label +// CreateLabel creates a new MCP tool for creating labels in GitHub repositories. func CreateLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("create_label", - mcp.WithDescription(t("TOOL_CREATE_LABEL_DESCRIPTION", "Create a new label in a GitHub repository.")), + mcp.WithDescription(t("TOOL_CREATE_LABEL_DESCRIPTION", "Create a new label in a GitHub repository. Used in the context of labels in relation to github resources, they are organizational tags used to categorize and filter issues and pull requests.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_CREATE_LABEL_TITLE", "Create label"), ReadOnlyHint: ToBoolPtr(false), From 78cb97cd65d89e705760aee6c577a97523b8c372 Mon Sep 17 00:00:00 2001 From: tonytrg Date: Fri, 19 Sep 2025 17:50:42 +0200 Subject: [PATCH 06/12] adding poc crud version of labels --- pkg/github/issues.go | 289 +++++++++++++++++++++++++++++++++++++++++++ pkg/github/tools.go | 9 +- 2 files changed, 294 insertions(+), 4 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index b11fb6289..1134abc13 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -2156,3 +2156,292 @@ func DeleteLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFu return mcp.NewToolResultText(fmt.Sprintf("label %s deleted successfully", name)), nil } } + +// CRUDLabel consolidates Create/Get/Update/Delete label operations into a single tool. +func CRUDLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { + return mcp.NewTool("crud_label", + mcp.WithDescription(t("TOOL_CRUD_LABEL_DESCRIPTION", "Create, read, update, or delete a label in a GitHub repository. Used in context of labels in relation to GitHub resources, they are organizational tags used to categorize and filter issues and pull requests. The use of parameters depends on the specific method selected.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_CRUD_LABEL_TITLE", "CRUD label"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("method", + mcp.Required(), + mcp.Description("Operation to perform: create, get, update or delete"), + mcp.Enum("create", "get", "update", "delete"), + ), + mcp.WithString("owner", + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Description("Repository name"), + ), + mcp.WithString("name", + mcp.Description("Label name (for get/update/delete or to create with this name)"), + ), + mcp.WithString("new_name", + mcp.Description("New name for the label (update only)"), + ), + mcp.WithString("color", + mcp.Description("Label color as a 6-character hex code without '#', e.g. 'f29513' (create/update)"), + ), + mcp.WithString("description", + mcp.Description("Label description (create/update)"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := RequiredParam[string](request, "method") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Normalize + method = strings.ToLower(method) + + // Basic params used across methods + owner, _ := OptionalParam[string](request, "owner") + repo, _ := OptionalParam[string](request, "repo") + name, _ := OptionalParam[string](request, "name") + newName, _ := OptionalParam[string](request, "new_name") + color, _ := OptionalParam[string](request, "color") + description, _ := OptionalParam[string](request, "description") + + client, err := getGQLClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + switch method { + case "create": + // Validate required params for create + if owner == "" { + return mcp.NewToolResultError("owner is required for create"), nil + } + if repo == "" { + return mcp.NewToolResultError("repo is required for create"), nil + } + if name == "" { + return mcp.NewToolResultError("name is required for create"), nil + } + if color == "" { + return mcp.NewToolResultError("color is required for create"), nil + } + + // Fetch repository node ID + var repoQuery struct { + Repository struct { + ID githubv4.ID + } `graphql:"repository(owner: $owner, name: $repo)"` + } + vars := map[string]any{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + } + if err := client.Query(ctx, &repoQuery, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find repository", err), nil + } + + input := githubv4.CreateLabelInput{ + RepositoryID: repoQuery.Repository.ID, + Name: githubv4.String(name), + } + if color != "" { + input.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 + } + + return mcp.NewToolResultText(fmt.Sprintf("label %s created successfully", mutation.CreateLabel.Label.Name)), nil + + case "get": + // Validate required params for get + if owner == "" { + return mcp.NewToolResultError("owner is required for get"), nil + } + if repo == "" { + return mcp.NewToolResultError("repo is required for get"), nil + } + if name == "" { + return mcp.NewToolResultError("name is required for get"), nil + } + + var query struct { + Repository struct { + Label struct { + ID githubv4.ID + Name githubv4.String + Color githubv4.String + Description githubv4.String + } `graphql:"label(name: $name)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } + + vars := map[string]any{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + "name": githubv4.String(name), + } + + if err := client.Query(ctx, &query, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", err), nil + } + + if query.Repository.Label.Name == "" { + return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil + } + + label := map[string]interface{}{ + "id": fmt.Sprintf("%v", query.Repository.Label.ID), + "name": string(query.Repository.Label.Name), + "color": string(query.Repository.Label.Color), + "description": string(query.Repository.Label.Description), + } + + out, err := json.Marshal(label) + if err != nil { + return nil, fmt.Errorf("failed to marshal label: %w", err) + } + + return mcp.NewToolResultText(string(out)), nil + + case "update": + // Validate required params for update + if owner == "" { + return mcp.NewToolResultError("owner is required for update"), nil + } + if repo == "" { + return mcp.NewToolResultError("repo is required for update"), nil + } + if name == "" { + return mcp.NewToolResultError("name is required for update"), nil + } + if newName == "" && color == "" && description == "" { + return mcp.NewToolResultError("at least one of new_name, color or description must be provided for update"), nil + } + + // Fetch the label to get its GQL ID + var query struct { + Repository struct { + Label struct { + ID githubv4.ID + Name githubv4.String + } `graphql:"label(name: $name)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } + + vars := map[string]any{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + "name": githubv4.String(name), + } + + if err := client.Query(ctx, &query, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", err), nil + } + + if query.Repository.Label.Name == "" { + return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil + } + + input := githubv4.UpdateLabelInput{ + ID: query.Repository.Label.ID, + } + 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 + } + + return mcp.NewToolResultText(fmt.Sprintf("label %s updated successfully", mutation.UpdateLabel.Label.Name)), nil + + case "delete": + // Validate required params for delete + if owner == "" { + return mcp.NewToolResultError("owner is required for delete"), nil + } + if repo == "" { + return mcp.NewToolResultError("repo is required for delete"), nil + } + if name == "" { + return mcp.NewToolResultError("name is required for delete"), nil + } + + var query struct { + Repository struct { + Label struct { + ID githubv4.ID + Name githubv4.String + } `graphql:"label(name: $name)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } + + vars := map[string]any{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + "name": githubv4.String(name), + } + + if err := client.Query(ctx, &query, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", err), nil + } + + if query.Repository.Label.Name == "" { + return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil + } + + input := githubv4.DeleteLabelInput{ + ID: query.Repository.Label.ID, + } + + var mutation struct { + DeleteLabel struct { + Typename githubv4.String `graphql:"__typename"` + } `graphql:"deleteLabel(input: $input)"` + } + + if err := client.Mutate(ctx, &mutation, input, nil); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to delete label", err), nil + } + + return mcp.NewToolResultText(fmt.Sprintf("label %s deleted successfully", name)), nil + } + + // Should not reach here; ensure a return value for the compiler + return mcp.NewToolResultError("method did not return a result"), nil + } +} diff --git a/pkg/github/tools.go b/pkg/github/tools.go index af4fcce96..89b11387a 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -61,7 +61,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(GetIssueComments(getClient, t)), toolsets.NewServerTool(ListIssueTypes(getClient, t)), toolsets.NewServerTool(ListSubIssues(getClient, t)), - toolsets.NewServerTool(GetLabel(getGQLClient, t)), + //toolsets.NewServerTool(GetLabel(getGQLClient, t)), ). AddWriteTools( toolsets.NewServerTool(CreateIssue(getClient, t)), @@ -71,9 +71,10 @@ 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(CreateLabel(getGQLClient, t)), - toolsets.NewServerTool(UpdateLabel(getGQLClient, t)), - toolsets.NewServerTool(DeleteLabel(getGQLClient, t)), + //toolsets.NewServerTool(CreateLabel(getGQLClient, t)), + //toolsets.NewServerTool(UpdateLabel(getGQLClient, t)), + //toolsets.NewServerTool(DeleteLabel(getGQLClient, t)), + toolsets.NewServerTool(CRUDLabel(getGQLClient, t)), ).AddPrompts( toolsets.NewServerPrompt(AssignCodingAgentPrompt(t)), toolsets.NewServerPrompt(IssueToFixWorkflowPrompt(t)), From fa6453d69c7670968fe25544182ea72e288476c2 Mon Sep 17 00:00:00 2001 From: tonytrg Date: Mon, 22 Sep 2025 10:22:54 +0200 Subject: [PATCH 07/12] adding list labels --- pkg/github/issues.go | 121 ++++++++++++++++++++++++++++++------------- 1 file changed, 84 insertions(+), 37 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 1134abc13..408fe67cf 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -2160,7 +2160,7 @@ func DeleteLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFu // CRUDLabel consolidates Create/Get/Update/Delete label operations into a single tool. func CRUDLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("crud_label", - mcp.WithDescription(t("TOOL_CRUD_LABEL_DESCRIPTION", "Create, read, update, or delete a label in a GitHub repository. Used in context of labels in relation to GitHub resources, they are organizational tags used to categorize and filter issues and pull requests. The use of parameters depends on the specific method selected.")), + mcp.WithDescription(t("TOOL_CRUD_LABEL_DESCRIPTION", "Create, read, update, or delete a label in a GitHub repository. Used in context of labels in relation to GitHub resources, they are organizational tags used to categorize and filter issues and pull requests. For 'get' method: if name is provided, retrieves a specific label; if name is omitted, lists all labels in the repository. The use of parameters depends on the specific method selected.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_CRUD_LABEL_TITLE", "CRUD label"), ReadOnlyHint: ToBoolPtr(false), @@ -2177,7 +2177,7 @@ func CRUDLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc mcp.Description("Repository name"), ), mcp.WithString("name", - mcp.Description("Label name (for get/update/delete or to create with this name)"), + mcp.Description("Label name (for get/update/delete or to create with this name). For 'get' method: optional - if provided, gets specific label; if omitted, lists all labels."), ), mcp.WithString("new_name", mcp.Description("New name for the label (update only)"), @@ -2276,48 +2276,94 @@ func CRUDLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc if repo == "" { return mcp.NewToolResultError("repo is required for get"), nil } - if name == "" { - return mcp.NewToolResultError("name is required for get"), nil - } - var query struct { - Repository struct { - Label struct { - ID githubv4.ID - Name githubv4.String - Color githubv4.String - Description githubv4.String - } `graphql:"label(name: $name)"` - } `graphql:"repository(owner: $owner, name: $repo)"` - } + if name != "" { + // Get specific label + var query struct { + Repository struct { + Label struct { + ID githubv4.ID + Name githubv4.String + Color githubv4.String + Description githubv4.String + } `graphql:"label(name: $name)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } - vars := map[string]any{ - "owner": githubv4.String(owner), - "repo": githubv4.String(repo), - "name": githubv4.String(name), - } + vars := map[string]any{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + "name": githubv4.String(name), + } - if err := client.Query(ctx, &query, vars); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", err), nil - } + if err := client.Query(ctx, &query, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", err), nil + } - if query.Repository.Label.Name == "" { - return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil - } + if query.Repository.Label.Name == "" { + return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil + } - label := map[string]interface{}{ - "id": fmt.Sprintf("%v", query.Repository.Label.ID), - "name": string(query.Repository.Label.Name), - "color": string(query.Repository.Label.Color), - "description": string(query.Repository.Label.Description), - } + label := map[string]interface{}{ + "id": fmt.Sprintf("%v", query.Repository.Label.ID), + "name": string(query.Repository.Label.Name), + "color": string(query.Repository.Label.Color), + "description": string(query.Repository.Label.Description), + } - out, err := json.Marshal(label) - if err != nil { - return nil, fmt.Errorf("failed to marshal label: %w", err) - } + out, err := json.Marshal(label) + if err != nil { + return nil, fmt.Errorf("failed to marshal label: %w", err) + } + + return mcp.NewToolResultText(string(out)), nil + } else { + // List all labels + var query struct { + Repository struct { + Labels struct { + Nodes []struct { + ID githubv4.ID + Name githubv4.String + Color githubv4.String + Description githubv4.String + } + TotalCount githubv4.Int + } `graphql:"labels(first: 100)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } + + vars := map[string]any{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + } + + if err := client.Query(ctx, &query, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to list labels", err), nil + } + + labels := make([]map[string]interface{}, len(query.Repository.Labels.Nodes)) + for i, labelNode := range query.Repository.Labels.Nodes { + labels[i] = map[string]interface{}{ + "id": fmt.Sprintf("%v", labelNode.ID), + "name": string(labelNode.Name), + "color": string(labelNode.Color), + "description": string(labelNode.Description), + } + } - return mcp.NewToolResultText(string(out)), nil + response := map[string]interface{}{ + "labels": labels, + "totalCount": int(query.Repository.Labels.TotalCount), + } + + out, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("failed to marshal labels: %w", err) + } + + return mcp.NewToolResultText(string(out)), nil + } case "update": // Validate required params for update @@ -2445,3 +2491,4 @@ func CRUDLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc return mcp.NewToolResultError("method did not return a result"), nil } } + From a62f0af1b80a6acdd11ec9ab3f31f37b234a37ac Mon Sep 17 00:00:00 2001 From: tonytrg Date: Mon, 22 Sep 2025 10:53:37 +0200 Subject: [PATCH 08/12] add experimental issue_labels --- pkg/github/issues.go | 530 ++++++++++++++++++++++++++++++++++++++++++- pkg/github/tools.go | 5 +- 2 files changed, 519 insertions(+), 16 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 408fe67cf..c107ca52c 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -42,7 +42,7 @@ const ( // When duplicateOf is non-zero, it fetches both the main issue and duplicate issue IDs in a single query. func fetchIssueIDs(ctx context.Context, gqlClient *githubv4.Client, owner, repo string, issueNumber int, duplicateOf int) (githubv4.ID, githubv4.ID, error) { // Build query variables common to both cases - vars := map[string]interface{}{ + vars := map[string]any{ "owner": githubv4.String(owner), "repo": githubv4.String(repo), "issueNumber": githubv4.Int(issueNumber), // #nosec G115 - issue numbers are always small positive integers @@ -991,7 +991,7 @@ func ListIssues(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun mcp.WithArray("labels", mcp.Description("Filter by labels"), mcp.Items( - map[string]interface{}{ + map[string]any{ "type": "string", }, ), @@ -1107,7 +1107,7 @@ func ListIssues(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil } - vars := map[string]interface{}{ + vars := map[string]any{ "owner": githubv4.String(owner), "repo": githubv4.String(repo), "states": states, @@ -1162,9 +1162,9 @@ func ListIssues(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun } // Create response with issues - response := map[string]interface{}{ + response := map[string]any{ "issues": issues, - "pageInfo": map[string]interface{}{ + "pageInfo": map[string]any{ "hasNextPage": pageInfo.HasNextPage, "hasPreviousPage": pageInfo.HasPreviousPage, "startCursor": string(pageInfo.StartCursor), @@ -1209,7 +1209,7 @@ func UpdateIssue(getClient GetClientFn, getGQLClient GetGQLClientFn, t translati mcp.WithArray("labels", mcp.Description("New labels"), mcp.Items( - map[string]interface{}{ + map[string]any{ "type": "string", }, ), @@ -1217,7 +1217,7 @@ func UpdateIssue(getClient GetClientFn, getGQLClient GetGQLClientFn, t translati mcp.WithArray("assignees", mcp.Description("New assignees"), mcp.Items( - map[string]interface{}{ + map[string]any{ "type": "string", }, ), @@ -1937,7 +1937,7 @@ func GetLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil } - label := map[string]interface{}{ + label := map[string]any{ "id": fmt.Sprintf("%v", query.Repository.Label.ID), "name": string(query.Repository.Label.Name), "color": string(query.Repository.Label.Color), @@ -2304,7 +2304,7 @@ func CRUDLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil } - label := map[string]interface{}{ + label := map[string]any{ "id": fmt.Sprintf("%v", query.Repository.Label.ID), "name": string(query.Repository.Label.Name), "color": string(query.Repository.Label.Color), @@ -2342,9 +2342,9 @@ func CRUDLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to list labels", err), nil } - labels := make([]map[string]interface{}, len(query.Repository.Labels.Nodes)) + labels := make([]map[string]any, len(query.Repository.Labels.Nodes)) for i, labelNode := range query.Repository.Labels.Nodes { - labels[i] = map[string]interface{}{ + labels[i] = map[string]any{ "id": fmt.Sprintf("%v", labelNode.ID), "name": string(labelNode.Name), "color": string(labelNode.Color), @@ -2352,7 +2352,7 @@ func CRUDLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc } } - response := map[string]interface{}{ + response := map[string]any{ "labels": labels, "totalCount": int(query.Repository.Labels.TotalCount), } @@ -2492,3 +2492,509 @@ func CRUDLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc } } +// IssueLabel manages labels on GitHub issues with list, add, and remove operations. +func IssueLabel(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { + return mcp.NewTool("issue_label", + mcp.WithDescription(t("TOOL_ISSUE_LABEL_DESCRIPTION", "Manage labels on GitHub issues. Use 'list' to get current labels on an issue, 'add' to add labels to an issue, or 'remove' to remove labels from an issue.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_ISSUE_LABEL_TITLE", "Manage issue labels"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("method", + mcp.Required(), + mcp.Description("Operation to perform: list, add, or remove"), + mcp.Enum("list", "add", "remove"), + ), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("issue_number", + mcp.Required(), + mcp.Description("Issue number"), + ), + mcp.WithArray("labels", + mcp.Description("Label names for add/remove operations (not used for list)"), + mcp.Items( + map[string]any{ + "type": "string", + }, + ), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := RequiredParam[string](request, "method") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + issueNumber, err := RequiredInt(request, "issue_number") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Normalize method + method = strings.ToLower(method) + + // Get labels parameter for add/remove operations + labels, err := OptionalStringArrayParam(request, "labels") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + switch method { + case "list": + // Get current labels on the issue + issue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber) + if err != nil { + return nil, fmt.Errorf("failed to get issue: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return mcp.NewToolResultError("failed to read response body"), nil + } + return mcp.NewToolResultError(fmt.Sprintf("failed to get issue: %s", string(body))), nil + } + + // Extract label information + issueLabels := make([]map[string]any, len(issue.Labels)) + for i, label := range issue.Labels { + issueLabels[i] = map[string]any{ + "id": fmt.Sprintf("%d", label.GetID()), + "name": label.GetName(), + "color": label.GetColor(), + "description": label.GetDescription(), + } + } + + response := map[string]any{ + "labels": issueLabels, + "count": len(issueLabels), + } + + out, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(out)), nil + + case "add": + // Validate labels parameter for add operation + if len(labels) == 0 { + return mcp.NewToolResultError("labels parameter is required for add operation"), nil + } + + // Add labels to the issue + updatedLabels, resp, err := client.Issues.AddLabelsToIssue(ctx, owner, repo, issueNumber, labels) + if err != nil { + return nil, fmt.Errorf("failed to add labels to issue: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return mcp.NewToolResultError("failed to read response body"), nil + } + return mcp.NewToolResultError(fmt.Sprintf("failed to add labels to issue: %s", string(body))), nil + } + + // Return the updated labels + issueLabels := make([]map[string]any, len(updatedLabels)) + for i, label := range updatedLabels { + issueLabels[i] = map[string]any{ + "id": fmt.Sprintf("%d", label.GetID()), + "name": label.GetName(), + "color": label.GetColor(), + "description": label.GetDescription(), + } + } + + response := map[string]any{ + "labels": issueLabels, + "count": len(issueLabels), + "added": labels, + } + + out, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(out)), nil + + case "remove": + // Validate labels parameter for remove operation + if len(labels) == 0 { + return mcp.NewToolResultError("labels parameter is required for remove operation"), nil + } + + // Remove labels from the issue + for _, labelName := range labels { + resp, err := client.Issues.RemoveLabelForIssue(ctx, owner, repo, issueNumber, labelName) + if err != nil { + return nil, fmt.Errorf("failed to remove label '%s' from issue: %w", labelName, err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound { + body, err := io.ReadAll(resp.Body) + if err != nil { + return mcp.NewToolResultError("failed to read response body"), nil + } + return mcp.NewToolResultError(fmt.Sprintf("failed to remove label '%s' from issue: %s", labelName, string(body))), nil + } + } + + // Get the updated issue to return current labels + issue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber) + if err != nil { + return nil, fmt.Errorf("failed to get updated issue: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + // Extract remaining label information + issueLabels := make([]map[string]any, len(issue.Labels)) + for i, label := range issue.Labels { + issueLabels[i] = map[string]any{ + "id": fmt.Sprintf("%d", label.GetID()), + "name": label.GetName(), + "color": label.GetColor(), + "description": label.GetDescription(), + } + } + + response := map[string]any{ + "labels": issueLabels, + "count": len(issueLabels), + "removed": labels, + } + + out, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(out)), nil + + default: + return mcp.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil + } + } +} + +// ListIssueLabels creates a tool to list current labels on a GitHub issue. +func ListIssueLabels(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { + return mcp.NewTool("list_issue_labels", + mcp.WithDescription(t("TOOL_LIST_ISSUE_LABELS_DESCRIPTION", "Get current labels on a GitHub issue.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_LIST_ISSUE_LABELS_TITLE", "List issue labels"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("issue_number", + mcp.Required(), + mcp.Description("Issue number"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + issueNumber, err := RequiredInt(request, "issue_number") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Get current labels on the issue + issue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber) + if err != nil { + return nil, fmt.Errorf("failed to get issue: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return mcp.NewToolResultError("failed to read response body"), nil + } + return mcp.NewToolResultError(fmt.Sprintf("failed to get issue: %s", string(body))), nil + } + + // Extract label information + issueLabels := make([]map[string]interface{}, len(issue.Labels)) + for i, label := range issue.Labels { + issueLabels[i] = map[string]interface{}{ + "id": fmt.Sprintf("%d", label.GetID()), + "name": label.GetName(), + "color": label.GetColor(), + "description": label.GetDescription(), + } + } + + response := map[string]interface{}{ + "labels": issueLabels, + "count": len(issueLabels), + } + + out, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(out)), nil + } +} + +// AddIssueLabels creates a tool to add labels to a GitHub issue. +func AddIssueLabels(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { + return mcp.NewTool("add_issue_labels", + mcp.WithDescription(t("TOOL_ADD_ISSUE_LABELS_DESCRIPTION", "Add labels to a GitHub issue.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_ADD_ISSUE_LABELS_TITLE", "Add issue labels"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("issue_number", + mcp.Required(), + mcp.Description("Issue number"), + ), + mcp.WithArray("labels", + mcp.Required(), + mcp.Description("Label names to add to the issue"), + mcp.Items( + map[string]interface{}{ + "type": "string", + }, + ), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + issueNumber, err := RequiredInt(request, "issue_number") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + labels, err := OptionalStringArrayParam(request, "labels") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + if len(labels) == 0 { + return mcp.NewToolResultError("at least one label is required"), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Add labels to the issue + updatedLabels, resp, err := client.Issues.AddLabelsToIssue(ctx, owner, repo, issueNumber, labels) + if err != nil { + return nil, fmt.Errorf("failed to add labels to issue: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return mcp.NewToolResultError("failed to read response body"), nil + } + return mcp.NewToolResultError(fmt.Sprintf("failed to add labels to issue: %s", string(body))), nil + } + + // Return the updated labels + issueLabels := make([]map[string]interface{}, len(updatedLabels)) + for i, label := range updatedLabels { + issueLabels[i] = map[string]interface{}{ + "id": fmt.Sprintf("%d", label.GetID()), + "name": label.GetName(), + "color": label.GetColor(), + "description": label.GetDescription(), + } + } + + response := map[string]interface{}{ + "labels": issueLabels, + "count": len(issueLabels), + "added": labels, + } + + out, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(out)), nil + } +} + +// RemoveIssueLabels creates a tool to remove labels from a GitHub issue. +func RemoveIssueLabels(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { + return mcp.NewTool("remove_issue_labels", + mcp.WithDescription(t("TOOL_REMOVE_ISSUE_LABELS_DESCRIPTION", "Remove labels from a GitHub issue.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_REMOVE_ISSUE_LABELS_TITLE", "Remove issue labels"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("issue_number", + mcp.Required(), + mcp.Description("Issue number"), + ), + mcp.WithArray("labels", + mcp.Required(), + mcp.Description("Label names to remove from the issue"), + mcp.Items( + map[string]interface{}{ + "type": "string", + }, + ), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + issueNumber, err := RequiredInt(request, "issue_number") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + labels, err := OptionalStringArrayParam(request, "labels") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + if len(labels) == 0 { + return mcp.NewToolResultError("at least one label is required"), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Remove labels from the issue + for _, labelName := range labels { + resp, err := client.Issues.RemoveLabelForIssue(ctx, owner, repo, issueNumber, labelName) + if err != nil { + return nil, fmt.Errorf("failed to remove label '%s' from issue: %w", labelName, err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound { + body, err := io.ReadAll(resp.Body) + if err != nil { + return mcp.NewToolResultError("failed to read response body"), nil + } + return mcp.NewToolResultError(fmt.Sprintf("failed to remove label '%s' from issue: %s", labelName, string(body))), nil + } + } + + // Get the updated issue to return current labels + issue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber) + if err != nil { + return nil, fmt.Errorf("failed to get updated issue: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + // Extract remaining label information + issueLabels := make([]map[string]interface{}, len(issue.Labels)) + for i, label := range issue.Labels { + issueLabels[i] = map[string]interface{}{ + "id": fmt.Sprintf("%d", label.GetID()), + "name": label.GetName(), + "color": label.GetColor(), + "description": label.GetDescription(), + } + } + + response := map[string]interface{}{ + "labels": issueLabels, + "count": len(issueLabels), + "removed": labels, + } + + out, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(out)), nil + } +} diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 89b11387a..5da9bc7ed 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -61,7 +61,6 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(GetIssueComments(getClient, t)), toolsets.NewServerTool(ListIssueTypes(getClient, t)), toolsets.NewServerTool(ListSubIssues(getClient, t)), - //toolsets.NewServerTool(GetLabel(getGQLClient, t)), ). AddWriteTools( toolsets.NewServerTool(CreateIssue(getClient, t)), @@ -71,10 +70,8 @@ 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(CreateLabel(getGQLClient, t)), - //toolsets.NewServerTool(UpdateLabel(getGQLClient, t)), - //toolsets.NewServerTool(DeleteLabel(getGQLClient, t)), toolsets.NewServerTool(CRUDLabel(getGQLClient, t)), + toolsets.NewServerTool(IssueLabel(getClient, t)), ).AddPrompts( toolsets.NewServerPrompt(AssignCodingAgentPrompt(t)), toolsets.NewServerPrompt(IssueToFixWorkflowPrompt(t)), From dd5373dd1321f7772d4ba7f9c36d2bea77918e3d Mon Sep 17 00:00:00 2001 From: tonytrg Date: Mon, 22 Sep 2025 14:25:54 +0200 Subject: [PATCH 09/12] label tools as seperate tools --- pkg/github/issues.go | 549 ------------------------------------------- pkg/github/tools.go | 9 +- 2 files changed, 7 insertions(+), 551 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index c107ca52c..4706ae3cb 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -2157,555 +2157,6 @@ func DeleteLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFu } } -// CRUDLabel consolidates Create/Get/Update/Delete label operations into a single tool. -func CRUDLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { - return mcp.NewTool("crud_label", - mcp.WithDescription(t("TOOL_CRUD_LABEL_DESCRIPTION", "Create, read, update, or delete a label in a GitHub repository. Used in context of labels in relation to GitHub resources, they are organizational tags used to categorize and filter issues and pull requests. For 'get' method: if name is provided, retrieves a specific label; if name is omitted, lists all labels in the repository. The use of parameters depends on the specific method selected.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_CRUD_LABEL_TITLE", "CRUD label"), - ReadOnlyHint: ToBoolPtr(false), - }), - mcp.WithString("method", - mcp.Required(), - mcp.Description("Operation to perform: create, get, update or delete"), - mcp.Enum("create", "get", "update", "delete"), - ), - mcp.WithString("owner", - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Description("Repository name"), - ), - mcp.WithString("name", - mcp.Description("Label name (for get/update/delete or to create with this name). For 'get' method: optional - if provided, gets specific label; if omitted, lists all labels."), - ), - mcp.WithString("new_name", - mcp.Description("New name for the label (update only)"), - ), - mcp.WithString("color", - mcp.Description("Label color as a 6-character hex code without '#', e.g. 'f29513' (create/update)"), - ), - mcp.WithString("description", - mcp.Description("Label description (create/update)"), - ), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := RequiredParam[string](request, "method") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - // Normalize - method = strings.ToLower(method) - - // Basic params used across methods - owner, _ := OptionalParam[string](request, "owner") - repo, _ := OptionalParam[string](request, "repo") - name, _ := OptionalParam[string](request, "name") - newName, _ := OptionalParam[string](request, "new_name") - color, _ := OptionalParam[string](request, "color") - description, _ := OptionalParam[string](request, "description") - - client, err := getGQLClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - - switch method { - case "create": - // Validate required params for create - if owner == "" { - return mcp.NewToolResultError("owner is required for create"), nil - } - if repo == "" { - return mcp.NewToolResultError("repo is required for create"), nil - } - if name == "" { - return mcp.NewToolResultError("name is required for create"), nil - } - if color == "" { - return mcp.NewToolResultError("color is required for create"), nil - } - - // Fetch repository node ID - var repoQuery struct { - Repository struct { - ID githubv4.ID - } `graphql:"repository(owner: $owner, name: $repo)"` - } - vars := map[string]any{ - "owner": githubv4.String(owner), - "repo": githubv4.String(repo), - } - if err := client.Query(ctx, &repoQuery, vars); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find repository", err), nil - } - - input := githubv4.CreateLabelInput{ - RepositoryID: repoQuery.Repository.ID, - Name: githubv4.String(name), - } - if color != "" { - input.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 - } - - return mcp.NewToolResultText(fmt.Sprintf("label %s created successfully", mutation.CreateLabel.Label.Name)), nil - - case "get": - // Validate required params for get - if owner == "" { - return mcp.NewToolResultError("owner is required for get"), nil - } - if repo == "" { - return mcp.NewToolResultError("repo is required for get"), nil - } - - if name != "" { - // Get specific label - var query struct { - Repository struct { - Label struct { - ID githubv4.ID - Name githubv4.String - Color githubv4.String - Description githubv4.String - } `graphql:"label(name: $name)"` - } `graphql:"repository(owner: $owner, name: $repo)"` - } - - vars := map[string]any{ - "owner": githubv4.String(owner), - "repo": githubv4.String(repo), - "name": githubv4.String(name), - } - - if err := client.Query(ctx, &query, vars); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", err), nil - } - - if query.Repository.Label.Name == "" { - return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil - } - - label := map[string]any{ - "id": fmt.Sprintf("%v", query.Repository.Label.ID), - "name": string(query.Repository.Label.Name), - "color": string(query.Repository.Label.Color), - "description": string(query.Repository.Label.Description), - } - - out, err := json.Marshal(label) - if err != nil { - return nil, fmt.Errorf("failed to marshal label: %w", err) - } - - return mcp.NewToolResultText(string(out)), nil - } else { - // List all labels - var query struct { - Repository struct { - Labels struct { - Nodes []struct { - ID githubv4.ID - Name githubv4.String - Color githubv4.String - Description githubv4.String - } - TotalCount githubv4.Int - } `graphql:"labels(first: 100)"` - } `graphql:"repository(owner: $owner, name: $repo)"` - } - - vars := map[string]any{ - "owner": githubv4.String(owner), - "repo": githubv4.String(repo), - } - - if err := client.Query(ctx, &query, vars); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to list labels", err), nil - } - - labels := make([]map[string]any, len(query.Repository.Labels.Nodes)) - for i, labelNode := range query.Repository.Labels.Nodes { - labels[i] = map[string]any{ - "id": fmt.Sprintf("%v", labelNode.ID), - "name": string(labelNode.Name), - "color": string(labelNode.Color), - "description": string(labelNode.Description), - } - } - - response := map[string]any{ - "labels": labels, - "totalCount": int(query.Repository.Labels.TotalCount), - } - - out, err := json.Marshal(response) - if err != nil { - return nil, fmt.Errorf("failed to marshal labels: %w", err) - } - - return mcp.NewToolResultText(string(out)), nil - } - - case "update": - // Validate required params for update - if owner == "" { - return mcp.NewToolResultError("owner is required for update"), nil - } - if repo == "" { - return mcp.NewToolResultError("repo is required for update"), nil - } - if name == "" { - return mcp.NewToolResultError("name is required for update"), nil - } - if newName == "" && color == "" && description == "" { - return mcp.NewToolResultError("at least one of new_name, color or description must be provided for update"), nil - } - - // Fetch the label to get its GQL ID - var query struct { - Repository struct { - Label struct { - ID githubv4.ID - Name githubv4.String - } `graphql:"label(name: $name)"` - } `graphql:"repository(owner: $owner, name: $repo)"` - } - - vars := map[string]any{ - "owner": githubv4.String(owner), - "repo": githubv4.String(repo), - "name": githubv4.String(name), - } - - if err := client.Query(ctx, &query, vars); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", err), nil - } - - if query.Repository.Label.Name == "" { - return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil - } - - input := githubv4.UpdateLabelInput{ - ID: query.Repository.Label.ID, - } - 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 - } - - return mcp.NewToolResultText(fmt.Sprintf("label %s updated successfully", mutation.UpdateLabel.Label.Name)), nil - - case "delete": - // Validate required params for delete - if owner == "" { - return mcp.NewToolResultError("owner is required for delete"), nil - } - if repo == "" { - return mcp.NewToolResultError("repo is required for delete"), nil - } - if name == "" { - return mcp.NewToolResultError("name is required for delete"), nil - } - - var query struct { - Repository struct { - Label struct { - ID githubv4.ID - Name githubv4.String - } `graphql:"label(name: $name)"` - } `graphql:"repository(owner: $owner, name: $repo)"` - } - - vars := map[string]any{ - "owner": githubv4.String(owner), - "repo": githubv4.String(repo), - "name": githubv4.String(name), - } - - if err := client.Query(ctx, &query, vars); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", err), nil - } - - if query.Repository.Label.Name == "" { - return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil - } - - input := githubv4.DeleteLabelInput{ - ID: query.Repository.Label.ID, - } - - var mutation struct { - DeleteLabel struct { - Typename githubv4.String `graphql:"__typename"` - } `graphql:"deleteLabel(input: $input)"` - } - - if err := client.Mutate(ctx, &mutation, input, nil); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to delete label", err), nil - } - - return mcp.NewToolResultText(fmt.Sprintf("label %s deleted successfully", name)), nil - } - - // Should not reach here; ensure a return value for the compiler - return mcp.NewToolResultError("method did not return a result"), nil - } -} - -// IssueLabel manages labels on GitHub issues with list, add, and remove operations. -func IssueLabel(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { - return mcp.NewTool("issue_label", - mcp.WithDescription(t("TOOL_ISSUE_LABEL_DESCRIPTION", "Manage labels on GitHub issues. Use 'list' to get current labels on an issue, 'add' to add labels to an issue, or 'remove' to remove labels from an issue.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_ISSUE_LABEL_TITLE", "Manage issue labels"), - ReadOnlyHint: ToBoolPtr(false), - }), - mcp.WithString("method", - mcp.Required(), - mcp.Description("Operation to perform: list, add, or remove"), - mcp.Enum("list", "add", "remove"), - ), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("issue_number", - mcp.Required(), - mcp.Description("Issue number"), - ), - mcp.WithArray("labels", - mcp.Description("Label names for add/remove operations (not used for list)"), - mcp.Items( - map[string]any{ - "type": "string", - }, - ), - ), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - method, err := RequiredParam[string](request, "method") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - owner, err := RequiredParam[string](request, "owner") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - repo, err := RequiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - issueNumber, err := RequiredInt(request, "issue_number") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - // Normalize method - method = strings.ToLower(method) - - // Get labels parameter for add/remove operations - labels, err := OptionalStringArrayParam(request, "labels") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - client, err := getClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - - switch method { - case "list": - // Get current labels on the issue - issue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber) - if err != nil { - return nil, fmt.Errorf("failed to get issue: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return mcp.NewToolResultError("failed to read response body"), nil - } - return mcp.NewToolResultError(fmt.Sprintf("failed to get issue: %s", string(body))), nil - } - - // Extract label information - issueLabels := make([]map[string]any, len(issue.Labels)) - for i, label := range issue.Labels { - issueLabels[i] = map[string]any{ - "id": fmt.Sprintf("%d", label.GetID()), - "name": label.GetName(), - "color": label.GetColor(), - "description": label.GetDescription(), - } - } - - response := map[string]any{ - "labels": issueLabels, - "count": len(issueLabels), - } - - out, err := json.Marshal(response) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(out)), nil - - case "add": - // Validate labels parameter for add operation - if len(labels) == 0 { - return mcp.NewToolResultError("labels parameter is required for add operation"), nil - } - - // Add labels to the issue - updatedLabels, resp, err := client.Issues.AddLabelsToIssue(ctx, owner, repo, issueNumber, labels) - if err != nil { - return nil, fmt.Errorf("failed to add labels to issue: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return mcp.NewToolResultError("failed to read response body"), nil - } - return mcp.NewToolResultError(fmt.Sprintf("failed to add labels to issue: %s", string(body))), nil - } - - // Return the updated labels - issueLabels := make([]map[string]any, len(updatedLabels)) - for i, label := range updatedLabels { - issueLabels[i] = map[string]any{ - "id": fmt.Sprintf("%d", label.GetID()), - "name": label.GetName(), - "color": label.GetColor(), - "description": label.GetDescription(), - } - } - - response := map[string]any{ - "labels": issueLabels, - "count": len(issueLabels), - "added": labels, - } - - out, err := json.Marshal(response) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(out)), nil - - case "remove": - // Validate labels parameter for remove operation - if len(labels) == 0 { - return mcp.NewToolResultError("labels parameter is required for remove operation"), nil - } - - // Remove labels from the issue - for _, labelName := range labels { - resp, err := client.Issues.RemoveLabelForIssue(ctx, owner, repo, issueNumber, labelName) - if err != nil { - return nil, fmt.Errorf("failed to remove label '%s' from issue: %w", labelName, err) - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound { - body, err := io.ReadAll(resp.Body) - if err != nil { - return mcp.NewToolResultError("failed to read response body"), nil - } - return mcp.NewToolResultError(fmt.Sprintf("failed to remove label '%s' from issue: %s", labelName, string(body))), nil - } - } - - // Get the updated issue to return current labels - issue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber) - if err != nil { - return nil, fmt.Errorf("failed to get updated issue: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - // Extract remaining label information - issueLabels := make([]map[string]any, len(issue.Labels)) - for i, label := range issue.Labels { - issueLabels[i] = map[string]any{ - "id": fmt.Sprintf("%d", label.GetID()), - "name": label.GetName(), - "color": label.GetColor(), - "description": label.GetDescription(), - } - } - - response := map[string]any{ - "labels": issueLabels, - "count": len(issueLabels), - "removed": labels, - } - - out, err := json.Marshal(response) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(out)), nil - - default: - return mcp.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil - } - } -} - // ListIssueLabels creates a tool to list current labels on a GitHub issue. func ListIssueLabels(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("list_issue_labels", diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 5da9bc7ed..d9ce2dfba 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -61,6 +61,8 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(GetIssueComments(getClient, t)), toolsets.NewServerTool(ListIssueTypes(getClient, t)), toolsets.NewServerTool(ListSubIssues(getClient, t)), + toolsets.NewServerTool(GetLabel(getGQLClient, t)), + toolsets.NewServerTool(ListIssueLabels(getClient, t)), ). AddWriteTools( toolsets.NewServerTool(CreateIssue(getClient, t)), @@ -70,8 +72,11 @@ 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(CRUDLabel(getGQLClient, t)), - toolsets.NewServerTool(IssueLabel(getClient, t)), + toolsets.NewServerTool(CreateLabel(getGQLClient, t)), + toolsets.NewServerTool(UpdateLabel(getGQLClient, t)), + toolsets.NewServerTool(DeleteLabel(getGQLClient, t)), + toolsets.NewServerTool(AddIssueLabels(getClient, t)), + toolsets.NewServerTool(RemoveIssueLabels(getClient, t)), ).AddPrompts( toolsets.NewServerPrompt(AssignCodingAgentPrompt(t)), toolsets.NewServerPrompt(IssueToFixWorkflowPrompt(t)), From c06f33c65e8f5bad9a83f925a4e1541e99ede0d5 Mon Sep 17 00:00:00 2001 From: tonytrg Date: Mon, 22 Sep 2025 14:47:59 +0200 Subject: [PATCH 10/12] update getlabel description --- pkg/github/issues.go | 109 +++++++++++++++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 24 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 4706ae3cb..b22fb58fb 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -1871,12 +1871,12 @@ func CreateLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFu } } -// Get label +// GetLabel handles both listing all labels and getting a specific label func GetLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { return mcp.NewTool("get_label", - mcp.WithDescription(t("TOOL_GET_LABEL_DESCRIPTION", "Get a label in a GitHub repository.")), + mcp.WithDescription(t("TOOL_GET_LABEL_DESCRIPTION", "Get a label from a specific repository. If no label name is provided, lists all labels in the repository.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_GET_LABEL_TITLE", "Get label"), + Title: t("TOOL_GET_LABEL_TITLE", "Get/List labels"), ReadOnlyHint: ToBoolPtr(true), }), mcp.WithString("owner", @@ -1888,8 +1888,7 @@ func GetLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) mcp.Description("Repository name"), ), mcp.WithString("name", - mcp.Required(), - mcp.Description("Name of the label to retrieve"), + mcp.Description("Name of the label to retrieve. If not provided, lists all labels in the repository."), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -1901,7 +1900,7 @@ func GetLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) if err != nil { return mcp.NewToolResultError(err.Error()), nil } - name, err := RequiredParam[string](request, "name") + name, err := OptionalParam[string](request, "name") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -1911,42 +1910,104 @@ func GetLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) return nil, fmt.Errorf("failed to get GitHub client: %w", err) } + // If name is provided, get specific label + if name != "" { + var query struct { + Repository struct { + Label struct { + ID githubv4.ID + Name githubv4.String + Color githubv4.String + Description githubv4.String + } `graphql:"label(name: $name)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + } + + vars := map[string]any{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + "name": githubv4.String(name), + } + + if err := client.Query(ctx, &query, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", err), nil + } + + // If label wasn't found, return a helpful error + if query.Repository.Label.Name == "" { + return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil + } + + label := map[string]any{ + "id": fmt.Sprintf("%v", query.Repository.Label.ID), + "name": string(query.Repository.Label.Name), + "color": string(query.Repository.Label.Color), + "description": string(query.Repository.Label.Description), + } + + out, err := json.Marshal(label) + if err != nil { + return nil, fmt.Errorf("failed to marshal label: %w", err) + } + + return mcp.NewToolResultText(string(out)), nil + } + + // If no name provided, list all labels var query struct { Repository struct { - Label struct { - ID githubv4.ID - Name githubv4.String - Color githubv4.String - Description githubv4.String - } `graphql:"label(name: $name)"` + Labels struct { + Nodes []struct { + ID githubv4.ID + Name githubv4.String + Color githubv4.String + Description githubv4.String + } + PageInfo struct { + HasNextPage githubv4.Boolean + HasPreviousPage githubv4.Boolean + StartCursor githubv4.String + EndCursor githubv4.String + } + TotalCount githubv4.Int + } `graphql:"labels(first: 100)"` } `graphql:"repository(owner: $owner, name: $repo)"` } vars := map[string]any{ "owner": githubv4.String(owner), "repo": githubv4.String(repo), - "name": githubv4.String(name), } if err := client.Query(ctx, &query, vars); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", err), nil + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to list labels", err), nil } - // If label wasn't found, return a helpful error - if query.Repository.Label.Name == "" { - return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil + var labels []map[string]any + for _, label := range query.Repository.Labels.Nodes { + labels = append(labels, map[string]any{ + "id": fmt.Sprintf("%v", label.ID), + "name": string(label.Name), + "color": string(label.Color), + "description": string(label.Description), + }) } - label := map[string]any{ - "id": fmt.Sprintf("%v", query.Repository.Label.ID), - "name": string(query.Repository.Label.Name), - "color": string(query.Repository.Label.Color), - "description": string(query.Repository.Label.Description), + response := map[string]any{ + "labels": labels, + "count": len(labels), + "pageInfo": map[string]any{ + "hasNextPage": bool(query.Repository.Labels.PageInfo.HasNextPage), + "hasPreviousPage": bool(query.Repository.Labels.PageInfo.HasPreviousPage), + "startCursor": string(query.Repository.Labels.PageInfo.StartCursor), + "endCursor": string(query.Repository.Labels.PageInfo.EndCursor), + }, + "totalCount": int(query.Repository.Labels.TotalCount), } - out, err := json.Marshal(label) + out, err := json.Marshal(response) if err != nil { - return nil, fmt.Errorf("failed to marshal label: %w", err) + return nil, fmt.Errorf("failed to marshal labels: %w", err) } return mcp.NewToolResultText(string(out)), nil From 977c1335738a61d6083ed1acf424731d04226148 Mon Sep 17 00:00:00 2001 From: tonytrg Date: Mon, 22 Sep 2025 14:48:47 +0200 Subject: [PATCH 11/12] adding better desc --- pkg/github/issues.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index b22fb58fb..c831ddf70 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -1872,9 +1872,9 @@ func CreateLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFu } // GetLabel handles both listing all labels and getting a specific label -func GetLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { - return mcp.NewTool("get_label", - mcp.WithDescription(t("TOOL_GET_LABEL_DESCRIPTION", "Get a label from a specific repository. If no label name is provided, lists all labels in the repository.")), +func GetLabels(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { + return mcp.NewTool("get_labels", + mcp.WithDescription(t("TOOL_GET_LABEL_DESCRIPTION", "Get a label from a specific repository. If no label name is provided, lists all labels.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: t("TOOL_GET_LABEL_TITLE", "Get/List labels"), ReadOnlyHint: ToBoolPtr(true), From 699b97c8d7a5be1379687a6d59517cfa5dc44804 Mon Sep 17 00:00:00 2001 From: tonytrg Date: Mon, 22 Sep 2025 15:09:28 +0200 Subject: [PATCH 12/12] adding chores --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ pkg/github/tools.go | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 891e63a81..c74eed92d 100644 --- a/README.md +++ b/README.md @@ -510,6 +510,12 @@ The following sets of tools are available (all are on by default): - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) +- **add_issue_labels** - Add issue labels + - `issue_number`: Issue number (number, required) + - `labels`: Label names to add to the issue (string[], required) + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - **add_sub_issue** - Add sub-issue - `issue_number`: The number of the parent issue (number, required) - `owner`: Repository owner (string, required) @@ -532,6 +538,18 @@ The following sets of tools are available (all are on by default): - `title`: Issue title (string, required) - `type`: Type of this issue (string, optional) +- **create_label** - Create label + - `color`: Label color as a 6-character hex code without '#', e.g. 'f29513' (string, required) + - `description`: Label description (string, optional) + - `name`: Name of the label to create (string, required) + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + +- **delete_label** - Delete label + - `name`: Name of the label to delete (string, required) + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - **get_issue** - Get issue details - `issue_number`: The number of the issue (number, required) - `owner`: The owner of the repository (string, required) @@ -544,6 +562,16 @@ The following sets of tools are available (all are on by default): - `perPage`: Results per page for pagination (min 1, max 100) (number, optional) - `repo`: Repository name (string, required) +- **get_labels** - Get/List labels + - `name`: Name of the label to retrieve. If not provided, lists all labels in the repository. (string, optional) + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + +- **list_issue_labels** - List issue labels + - `issue_number`: Issue number (number, required) + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - **list_issue_types** - List available issue types - `owner`: The organization owner of the repository (string, required) @@ -565,6 +593,12 @@ 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) - `repo`: Repository name (string, required) +- **remove_issue_labels** - Remove issue labels + - `issue_number`: Issue number (number, required) + - `labels`: Label names to remove from the issue (string[], required) + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - **remove_sub_issue** - Remove sub-issue - `issue_number`: The number of the parent issue (number, required) - `owner`: Repository owner (string, required) @@ -602,6 +636,14 @@ The following sets of tools are available (all are on by default): - `title`: New title (string, optional) - `type`: New issue type (string, optional) +- **update_label** - Update label + - `color`: New label color as a 6-character hex code without '#', e.g. 'f29513' (string, optional) + - `description`: New label description (string, optional) + - `name`: Name of the existing label to update (string, required) + - `new_name`: New name for the label (string, optional) + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) +
diff --git a/pkg/github/tools.go b/pkg/github/tools.go index d9ce2dfba..6ecc94900 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -61,7 +61,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(GetIssueComments(getClient, t)), toolsets.NewServerTool(ListIssueTypes(getClient, t)), toolsets.NewServerTool(ListSubIssues(getClient, t)), - toolsets.NewServerTool(GetLabel(getGQLClient, t)), + toolsets.NewServerTool(GetLabels(getGQLClient, t)), toolsets.NewServerTool(ListIssueLabels(getClient, t)), ). AddWriteTools(