From 64b6a6893efb5d3cf8f9ca68f05c90de43c83693 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:12:50 +0000 Subject: [PATCH 1/3] Initial plan From fc21c1c118b49a3ff3676e3278874fa547190fa9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:29:20 +0000 Subject: [PATCH 2/3] Migrate labels toolset from mark3labs/mcp-go to modelcontextprotocol/go-sdk Co-authored-by: omgitsads <4619+omgitsads@users.noreply.github.com> --- pkg/github/__toolsnaps__/get_label.snap | 30 +- pkg/github/__toolsnaps__/label_write.snap | 47 +- pkg/github/__toolsnaps__/list_label.snap | 24 +- pkg/github/labels.go | 621 +++++++++++----------- pkg/github/labels_test.go | 26 +- 5 files changed, 381 insertions(+), 367 deletions(-) diff --git a/pkg/github/__toolsnaps__/get_label.snap b/pkg/github/__toolsnaps__/get_label.snap index a6b72c4eb..8541044d0 100644 --- a/pkg/github/__toolsnaps__/get_label.snap +++ b/pkg/github/__toolsnaps__/get_label.snap @@ -1,30 +1,30 @@ { "annotations": { - "title": "Get a specific label from a repository.", - "readOnlyHint": true + "readOnlyHint": true, + "title": "Get a specific label from a repository." }, "description": "Get a specific label from a repository.", "inputSchema": { + "type": "object", + "required": [ + "owner", + "repo", + "name" + ], "properties": { "name": { - "description": "Label name.", - "type": "string" + "type": "string", + "description": "Label name." }, "owner": { - "description": "Repository owner (username or organization name)", - "type": "string" + "type": "string", + "description": "Repository owner (username or organization name)" }, "repo": { - "description": "Repository name", - "type": "string" + "type": "string", + "description": "Repository name" } - }, - "required": [ - "owner", - "repo", - "name" - ], - "type": "object" + } }, "name": "get_label" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/label_write.snap b/pkg/github/__toolsnaps__/label_write.snap index 12d0bd441..879817442 100644 --- a/pkg/github/__toolsnaps__/label_write.snap +++ b/pkg/github/__toolsnaps__/label_write.snap @@ -1,52 +1,51 @@ { "annotations": { - "title": "Write operations on repository labels.", - "readOnlyHint": false + "title": "Write operations on repository labels." }, "description": "Perform write operations on repository labels. To set labels on issues, use the 'update_issue' tool.", "inputSchema": { + "type": "object", + "required": [ + "method", + "owner", + "repo", + "name" + ], "properties": { "color": { - "description": "Label color as 6-character hex code without '#' prefix (e.g., 'f29513'). Required for 'create', optional for 'update'.", - "type": "string" + "type": "string", + "description": "Label color as 6-character hex code without '#' prefix (e.g., 'f29513'). Required for 'create', optional for 'update'." }, "description": { - "description": "Label description text. Optional for 'create' and 'update'.", - "type": "string" + "type": "string", + "description": "Label description text. Optional for 'create' and 'update'." }, "method": { + "type": "string", "description": "Operation to perform: 'create', 'update', or 'delete'", "enum": [ "create", "update", "delete" - ], - "type": "string" + ] }, "name": { - "description": "Label name - required for all operations", - "type": "string" + "type": "string", + "description": "Label name - required for all operations" }, "new_name": { - "description": "New name for the label (used only with 'update' method to rename)", - "type": "string" + "type": "string", + "description": "New name for the label (used only with 'update' method to rename)" }, "owner": { - "description": "Repository owner (username or organization name)", - "type": "string" + "type": "string", + "description": "Repository owner (username or organization name)" }, "repo": { - "description": "Repository name", - "type": "string" + "type": "string", + "description": "Repository name" } - }, - "required": [ - "method", - "owner", - "repo", - "name" - ], - "type": "object" + } }, "name": "label_write" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_label.snap b/pkg/github/__toolsnaps__/list_label.snap index 1b6c0108f..0b4f3b20c 100644 --- a/pkg/github/__toolsnaps__/list_label.snap +++ b/pkg/github/__toolsnaps__/list_label.snap @@ -1,25 +1,25 @@ { "annotations": { - "title": "List labels from a repository.", - "readOnlyHint": true + "readOnlyHint": true, + "title": "List labels from a repository." }, "description": "List labels from a repository", "inputSchema": { + "type": "object", + "required": [ + "owner", + "repo" + ], "properties": { "owner": { - "description": "Repository owner (username or organization name) - required for all operations", - "type": "string" + "type": "string", + "description": "Repository owner (username or organization name) - required for all operations" }, "repo": { - "description": "Repository name - required for all operations", - "type": "string" + "type": "string", + "description": "Repository name - required for all operations" } - }, - "required": [ - "owner", - "repo" - ], - "type": "object" + } }, "name": "list_label" } \ No newline at end of file diff --git a/pkg/github/labels.go b/pkg/github/labels.go index 42b53fc6d..25ac9f7fe 100644 --- a/pkg/github/labels.go +++ b/pkg/github/labels.go @@ -1,5 +1,3 @@ -//go:build ignore - package github import ( @@ -10,353 +8,384 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/github/github-mcp-server/pkg/utils" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/shurcooL/githubv4" ) // GetLabel retrieves a specific label by name from a GitHub repository -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 specific label from a repository.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_GET_LABEL_TITLE", "Get a specific label from a repository."), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner (username or organization name)"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithString("name", - mcp.Required(), - mcp.Description("Label name."), - ), - ), - 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 - } +func GetLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + tool := mcp.Tool{ + Name: "get_label", + Description: t("TOOL_GET_LABEL_DESCRIPTION", "Get a specific label from a repository."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_GET_LABEL_TITLE", "Get a specific label from a repository."), + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner (username or organization name)", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "name": { + Type: "string", + Description: "Label name.", + }, + }, + Required: []string{"owner", "repo", "name"}, + }, + } - repo, err := RequiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } + handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - name, err := RequiredParam[string](request, "name") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - var query struct { - Repository struct { - Label struct { + name, err := RequiredParam[string](args, "name") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, 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), + } + + client, err := getGQLClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + if err := client.Query(ctx, &query, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", err), nil, nil + } + + if query.Repository.Label.Name == "" { + return utils.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil, 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, nil, fmt.Errorf("failed to marshal label: %w", err) + } + + return utils.NewToolResultText(string(out)), nil, nil + }) + + return tool, handler +} + +// ListLabels lists labels from a repository +func ListLabels(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + tool := mcp.Tool{ + Name: "list_label", + Description: t("TOOL_LIST_LABEL_DESCRIPTION", "List labels from a repository"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_LIST_LABEL_DESCRIPTION", "List labels from a repository."), + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner (username or organization name) - required for all operations", + }, + "repo": { + Type: "string", + Description: "Repository name - required for all operations", + }, + }, + Required: []string{"owner", "repo"}, + }, + } + + handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + client, err := getGQLClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + var query struct { + Repository struct { + Labels struct { + Nodes []struct { ID githubv4.ID Name githubv4.String Color githubv4.String Description githubv4.String - } `graphql:"label(name: $name)"` - } `graphql:"repository(owner: $owner, name: $repo)"` - } + } + 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), - } + vars := map[string]any{ + "owner": githubv4.String(owner), + "repo": githubv4.String(repo), + } - client, err := getGQLClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) - } + if err := client.Query(ctx, &query, vars); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to list labels", err), nil, nil + } - if err := client.Query(ctx, &query, vars); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find label", 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), } + } - if query.Repository.Label.Name == "" { - return mcp.NewToolResultError(fmt.Sprintf("label '%s' not found in %s/%s", name, owner, repo)), nil - } + response := map[string]any{ + "labels": labels, + "totalCount": int(query.Repository.Labels.TotalCount), + } - 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(response) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal labels: %w", err) + } - out, err := json.Marshal(label) - if err != nil { - return nil, fmt.Errorf("failed to marshal label: %w", err) - } + return utils.NewToolResultText(string(out)), nil, nil + }) - return mcp.NewToolResultText(string(out)), nil - } + return tool, handler } -// ListLabels lists labels from a repository -func ListLabels(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { - return mcp.NewTool( - "list_label", - mcp.WithDescription(t("TOOL_LIST_LABEL_DESCRIPTION", "List labels from a repository")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_LIST_LABEL_DESCRIPTION", "List labels from a repository."), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner (username or organization name) - required for all operations"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name - required for all operations"), - ), - ), - 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 - } +// LabelWrite handles create, update, and delete operations for GitHub labels +func LabelWrite(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + tool := mcp.Tool{ + Name: "label_write", + Description: t("TOOL_LABEL_WRITE_DESCRIPTION", "Perform write operations on repository labels. To set labels on issues, use the 'update_issue' tool."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_LABEL_WRITE_TITLE", "Write operations on repository labels."), + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "method": { + Type: "string", + Description: "Operation to perform: 'create', 'update', or 'delete'", + Enum: []any{"create", "update", "delete"}, + }, + "owner": { + Type: "string", + Description: "Repository owner (username or organization name)", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "name": { + Type: "string", + Description: "Label name - required for all operations", + }, + "new_name": { + Type: "string", + Description: "New name for the label (used only with 'update' method to rename)", + }, + "color": { + Type: "string", + Description: "Label color as 6-character hex code without '#' prefix (e.g., 'f29513'). Required for 'create', optional for 'update'.", + }, + "description": { + Type: "string", + Description: "Label description text. Optional for 'create' and 'update'.", + }, + }, + Required: []string{"method", "owner", "repo", "name"}, + }, + } - repo, err := RequiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil + handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + // Get and validate required parameters + method, err := RequiredParam[string](args, "method") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + method = strings.ToLower(method) + + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + name, err := RequiredParam[string](args, "name") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + // Get optional parameters + newName, _ := OptionalParam[string](args, "new_name") + color, _ := OptionalParam[string](args, "color") + description, _ := OptionalParam[string](args, "description") + + client, err := getGQLClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + switch method { + case "create": + // Validate required params for create + if color == "" { + return utils.NewToolResultError("color is required for create"), nil, nil } - client, err := getGQLClient(ctx) + // Get repository ID + repoID, err := getRepositoryID(ctx, client, owner, repo) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find repository", err), nil, nil } - 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)"` + input := githubv4.CreateLabelInput{ + RepositoryID: repoID, + Name: githubv4.String(name), + Color: githubv4.String(color), } - - vars := map[string]any{ - "owner": githubv4.String(owner), - "repo": githubv4.String(repo), + if description != "" { + d := githubv4.String(description) + input.Description = &d } - if err := client.Query(ctx, &query, vars); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to list labels", err), nil + var mutation struct { + CreateLabel struct { + Label struct { + Name githubv4.String + ID githubv4.ID + } + } `graphql:"createLabel(input: $input)"` } - 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), - } + if err := client.Mutate(ctx, &mutation, input, nil); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to create label", err), nil, nil } - response := map[string]any{ - "labels": labels, - "totalCount": int(query.Repository.Labels.TotalCount), + return utils.NewToolResultText(fmt.Sprintf("label '%s' created successfully", mutation.CreateLabel.Label.Name)), nil, nil + + case "update": + // Validate required params for update + if newName == "" && color == "" && description == "" { + return utils.NewToolResultError("at least one of new_name, color, or description must be provided for update"), nil, nil } - out, err := json.Marshal(response) + // Get the label ID + labelID, err := getLabelID(ctx, client, owner, repo, name) if err != nil { - return nil, fmt.Errorf("failed to marshal labels: %w", err) + return utils.NewToolResultError(err.Error()), nil, nil } - return mcp.NewToolResultText(string(out)), nil - } -} - -// LabelWrite handles create, update, and delete operations for GitHub labels -func LabelWrite(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) { - return mcp.NewTool( - "label_write", - mcp.WithDescription(t("TOOL_LABEL_WRITE_DESCRIPTION", "Perform write operations on repository labels. To set labels on issues, use the 'update_issue' tool.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ - Title: t("TOOL_LABEL_WRITE_TITLE", "Write operations on repository labels."), - ReadOnlyHint: ToBoolPtr(false), - }), - mcp.WithString("method", - mcp.Required(), - mcp.Description("Operation to perform: 'create', 'update', or 'delete'"), - mcp.Enum("create", "update", "delete"), - ), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner (username or organization name)"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithString("name", - mcp.Required(), - mcp.Description("Label name - required for all operations"), - ), - mcp.WithString("new_name", - mcp.Description("New name for the label (used only with 'update' method to rename)"), - ), - mcp.WithString("color", - mcp.Description("Label color as 6-character hex code without '#' prefix (e.g., 'f29513'). Required for 'create', optional for 'update'."), - ), - mcp.WithString("description", - mcp.Description("Label description text. Optional for 'create' and 'update'."), - ), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - // Get and validate required parameters - method, err := RequiredParam[string](request, "method") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil + input := githubv4.UpdateLabelInput{ + ID: labelID, + } + if newName != "" { + n := githubv4.String(newName) + input.Name = &n + } + if color != "" { + c := githubv4.String(color) + input.Color = &c + } + if description != "" { + d := githubv4.String(description) + input.Description = &d } - method = strings.ToLower(method) - owner, err := RequiredParam[string](request, "owner") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil + var mutation struct { + UpdateLabel struct { + Label struct { + Name githubv4.String + ID githubv4.ID + } + } `graphql:"updateLabel(input: $input)"` } - repo, err := RequiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil + if err := client.Mutate(ctx, &mutation, input, nil); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to update label", err), nil, nil } - name, err := RequiredParam[string](request, "name") + return utils.NewToolResultText(fmt.Sprintf("label '%s' updated successfully", mutation.UpdateLabel.Label.Name)), nil, nil + + case "delete": + // Get the label ID + labelID, err := getLabelID(ctx, client, owner, repo, name) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - // Get optional parameters - newName, _ := OptionalParam[string](request, "new_name") - color, _ := OptionalParam[string](request, "color") - description, _ := OptionalParam[string](request, "description") + input := githubv4.DeleteLabelInput{ + ID: labelID, + } - client, err := getGQLClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + var mutation struct { + DeleteLabel struct { + ClientMutationID githubv4.String + } `graphql:"deleteLabel(input: $input)"` } - switch method { - case "create": - // Validate required params for create - if color == "" { - return mcp.NewToolResultError("color is required for create"), nil - } - - // Get repository ID - repoID, err := getRepositoryID(ctx, client, owner, repo) - if err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find repository", err), nil - } - - input := githubv4.CreateLabelInput{ - RepositoryID: repoID, - Name: githubv4.String(name), - Color: githubv4.String(color), - } - if description != "" { - d := githubv4.String(description) - input.Description = &d - } - - var mutation struct { - CreateLabel struct { - Label struct { - Name githubv4.String - ID githubv4.ID - } - } `graphql:"createLabel(input: $input)"` - } - - if err := client.Mutate(ctx, &mutation, input, nil); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to create label", err), nil - } - - return mcp.NewToolResultText(fmt.Sprintf("label '%s' created successfully", mutation.CreateLabel.Label.Name)), nil - - case "update": - // Validate required params for update - if newName == "" && color == "" && description == "" { - return mcp.NewToolResultError("at least one of new_name, color, or description must be provided for update"), nil - } - - // Get the label ID - labelID, err := getLabelID(ctx, client, owner, repo, name) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - input := githubv4.UpdateLabelInput{ - ID: labelID, - } - if newName != "" { - n := githubv4.String(newName) - input.Name = &n - } - if color != "" { - c := githubv4.String(color) - input.Color = &c - } - if description != "" { - d := githubv4.String(description) - input.Description = &d - } - - var mutation struct { - UpdateLabel struct { - Label struct { - Name githubv4.String - ID githubv4.ID - } - } `graphql:"updateLabel(input: $input)"` - } - - if err := client.Mutate(ctx, &mutation, input, nil); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to update label", err), nil - } - - return mcp.NewToolResultText(fmt.Sprintf("label '%s' updated successfully", mutation.UpdateLabel.Label.Name)), nil - - case "delete": - // Get the label ID - labelID, err := getLabelID(ctx, client, owner, repo, name) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - input := githubv4.DeleteLabelInput{ - ID: labelID, - } - - var mutation struct { - DeleteLabel struct { - ClientMutationID githubv4.String - } `graphql:"deleteLabel(input: $input)"` - } - - if err := client.Mutate(ctx, &mutation, input, nil); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to delete label", err), nil - } - - return mcp.NewToolResultText(fmt.Sprintf("label '%s' deleted successfully", name)), nil - - default: - return mcp.NewToolResultError(fmt.Sprintf("unknown method: %s. Supported methods are: create, update, delete", method)), nil + if err := client.Mutate(ctx, &mutation, input, nil); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to delete label", err), nil, nil } + + return utils.NewToolResultText(fmt.Sprintf("label '%s' deleted successfully", name)), nil, nil + + default: + return utils.NewToolResultError(fmt.Sprintf("unknown method: %s. Supported methods are: create, update, delete", method)), nil, nil } + }) + + return tool, handler } // Helper function to get repository ID diff --git a/pkg/github/labels_test.go b/pkg/github/labels_test.go index 5055364f0..12d447d72 100644 --- a/pkg/github/labels_test.go +++ b/pkg/github/labels_test.go @@ -1,5 +1,3 @@ -//go:build ignore - package github import ( @@ -25,10 +23,7 @@ func TestGetLabel(t *testing.T) { assert.Equal(t, "get_label", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "name") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "name"}) + assert.True(t, tool.Annotations.ReadOnlyHint, "get_label tool should be read-only") tests := []struct { name string @@ -122,7 +117,7 @@ func TestGetLabel(t *testing.T) { _, handler := GetLabel(stubGetGQLClientFn(client), translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) assert.NotNil(t, result) @@ -150,9 +145,7 @@ func TestListLabels(t *testing.T) { assert.Equal(t, "list_label", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) + assert.True(t, tool.Annotations.ReadOnlyHint, "list_label tool should be read-only") tests := []struct { name string @@ -219,7 +212,7 @@ func TestListLabels(t *testing.T) { _, handler := ListLabels(stubGetGQLClientFn(client), translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) assert.NotNil(t, result) @@ -247,14 +240,7 @@ func TestWriteLabel(t *testing.T) { assert.Equal(t, "label_write", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "method") - assert.Contains(t, tool.InputSchema.Properties, "owner") - assert.Contains(t, tool.InputSchema.Properties, "repo") - assert.Contains(t, tool.InputSchema.Properties, "name") - assert.Contains(t, tool.InputSchema.Properties, "new_name") - assert.Contains(t, tool.InputSchema.Properties, "color") - assert.Contains(t, tool.InputSchema.Properties, "description") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"method", "owner", "repo", "name"}) + assert.False(t, tool.Annotations.ReadOnlyHint, "label_write tool should not be read-only") tests := []struct { name string @@ -474,7 +460,7 @@ func TestWriteLabel(t *testing.T) { _, handler := LabelWrite(stubGetGQLClientFn(client), translations.NullTranslationHelper) request := createMCPRequest(tc.requestArgs) - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) require.NoError(t, err) assert.NotNil(t, result) From e3c5127e413f4b03bcaa8d95ec9e1894765e3755 Mon Sep 17 00:00:00 2001 From: LuluBeatson Date: Wed, 19 Nov 2025 12:50:57 +0000 Subject: [PATCH 3/3] re-add labels toolset --- pkg/github/tools.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 95b36f8b9..3c0a4e35a 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -346,17 +346,17 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG // toolsets.NewServerTool(StarRepository(getClient, t)), // toolsets.NewServerTool(UnstarRepository(getClient, t)), // ) - // labels := toolsets.NewToolset(ToolsetLabels.ID, ToolsetLabels.Description). - // AddReadTools( - // // get - // toolsets.NewServerTool(GetLabel(getGQLClient, t)), - // // list labels on repo or issue - // toolsets.NewServerTool(ListLabels(getGQLClient, t)), - // ). - // AddWriteTools( - // // create or update - // toolsets.NewServerTool(LabelWrite(getGQLClient, t)), - // ) + labels := toolsets.NewToolset(ToolsetLabels.ID, ToolsetLabels.Description). + AddReadTools( + // get + toolsets.NewServerTool(GetLabel(getGQLClient, t)), + // list labels on repo or issue + toolsets.NewServerTool(ListLabels(getGQLClient, t)), + ). + AddWriteTools( + // create or update + toolsets.NewServerTool(LabelWrite(getGQLClient, t)), + ) // Add toolsets to the group tsg.AddToolset(contextTools) @@ -377,7 +377,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG // tsg.AddToolset(securityAdvisories) // tsg.AddToolset(projects) // tsg.AddToolset(stargazers) - // tsg.AddToolset(labels) + tsg.AddToolset(labels) return tsg }