diff --git a/pkg/github/dynamic_tools.go b/pkg/github/dynamic_tools.go index 75d74f676..9118c1c45 100644 --- a/pkg/github/dynamic_tools.go +++ b/pkg/github/dynamic_tools.go @@ -20,27 +20,27 @@ func ToolsetEnum(toolsetGroup *toolsets.ToolsetGroup) []any { return toolsetNames } -func EnableToolset(s *mcp.Server, toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ - Name: "enable_toolset", - Description: t("TOOL_ENABLE_TOOLSET_DESCRIPTION", "Enable one of the sets of tools the GitHub MCP server provides, use get_toolset_tools and list_available_toolsets first to see what this will enable"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_ENABLE_TOOLSET_USER_TITLE", "Enable a toolset"), - // Not modifying GitHub data so no need to show a warning - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "toolset": { - Type: "string", - Description: "The name of the toolset to enable", - Enum: ToolsetEnum(toolsetGroup), - }, +func EnableToolset(s *mcp.Server, toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) toolsets.ServerTool { + return toolsets.NewServerToolLegacy(mcp.Tool{ + Name: "enable_toolset", + Description: t("TOOL_ENABLE_TOOLSET_DESCRIPTION", "Enable one of the sets of tools the GitHub MCP server provides, use get_toolset_tools and list_available_toolsets first to see what this will enable"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_ENABLE_TOOLSET_USER_TITLE", "Enable a toolset"), + // Not modifying GitHub data so no need to show a warning + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "toolset": { + Type: "string", + Description: "The name of the toolset to enable", + Enum: ToolsetEnum(toolsetGroup), }, - Required: []string{"toolset"}, }, + Required: []string{"toolset"}, }, + }, mcp.ToolHandlerFor[map[string]any, any](func(_ context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { // We need to convert the toolsets back to a map for JSON serialization toolsetName, err := RequiredParam[string](args, "toolset") @@ -64,22 +64,22 @@ func EnableToolset(s *mcp.Server, toolsetGroup *toolsets.ToolsetGroup, t transla toolset.RegisterTools(s) return utils.NewToolResultText(fmt.Sprintf("Toolset %s enabled", toolsetName)), nil, nil - }) + })) } -func ListAvailableToolsets(toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ - Name: "list_available_toolsets", - Description: t("TOOL_LIST_AVAILABLE_TOOLSETS_DESCRIPTION", "List all available toolsets this GitHub MCP server can offer, providing the enabled status of each. Use this when a task could be achieved with a GitHub tool and the currently available tools aren't enough. Call get_toolset_tools with these toolset names to discover specific tools you can call"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_LIST_AVAILABLE_TOOLSETS_USER_TITLE", "List available toolsets"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{}, - }, +func ListAvailableToolsets(toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) toolsets.ServerTool { + return toolsets.NewServerToolLegacy(mcp.Tool{ + Name: "list_available_toolsets", + Description: t("TOOL_LIST_AVAILABLE_TOOLSETS_DESCRIPTION", "List all available toolsets this GitHub MCP server can offer, providing the enabled status of each. Use this when a task could be achieved with a GitHub tool and the currently available tools aren't enough. Call get_toolset_tools with these toolset names to discover specific tools you can call"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_LIST_AVAILABLE_TOOLSETS_USER_TITLE", "List available toolsets"), + ReadOnlyHint: true, }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{}, + }, + }, mcp.ToolHandlerFor[map[string]any, any](func(_ context.Context, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) { // We need to convert the toolsetGroup back to a map for JSON serialization @@ -103,29 +103,29 @@ func ListAvailableToolsets(toolsetGroup *toolsets.ToolsetGroup, t translations.T } return utils.NewToolResultText(string(r)), nil, nil - }) + })) } -func GetToolsetsTools(toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ - Name: "get_toolset_tools", - Description: t("TOOL_GET_TOOLSET_TOOLS_DESCRIPTION", "Lists all the capabilities that are enabled with the specified toolset, use this to get clarity on whether enabling a toolset would help you to complete a task"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_TOOLSET_TOOLS_USER_TITLE", "List all tools in a toolset"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "toolset": { - Type: "string", - Description: "The name of the toolset you want to get the tools for", - Enum: ToolsetEnum(toolsetGroup), - }, +func GetToolsetsTools(toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) toolsets.ServerTool { + return toolsets.NewServerToolLegacy(mcp.Tool{ + Name: "get_toolset_tools", + Description: t("TOOL_GET_TOOLSET_TOOLS_DESCRIPTION", "Lists all the capabilities that are enabled with the specified toolset, use this to get clarity on whether enabling a toolset would help you to complete a task"), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_GET_TOOLSET_TOOLS_USER_TITLE", "List all tools in a toolset"), + ReadOnlyHint: true, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "toolset": { + Type: "string", + Description: "The name of the toolset you want to get the tools for", + Enum: ToolsetEnum(toolsetGroup), }, - Required: []string{"toolset"}, }, + Required: []string{"toolset"}, }, + }, mcp.ToolHandlerFor[map[string]any, any](func(_ context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { // We need to convert the toolsetGroup back to a map for JSON serialization toolsetName, err := RequiredParam[string](args, "toolset") @@ -154,5 +154,5 @@ func GetToolsetsTools(toolsetGroup *toolsets.ToolsetGroup, t translations.Transl } return utils.NewToolResultText(string(r)), nil, nil - }) + })) } diff --git a/pkg/github/labels.go b/pkg/github/labels.go index 25ac9f7fe..0c18c83e4 100644 --- a/pkg/github/labels.go +++ b/pkg/github/labels.go @@ -7,6 +7,7 @@ import ( "strings" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/toolsets" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/jsonschema-go/jsonschema" @@ -15,377 +16,380 @@ import ( ) // GetLabel retrieves a specific label by name from a GitHub repository -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.", +func GetLabel(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + 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"}, }, - Required: []string{"owner", "repo", "name"}, }, - } - - 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 - } - - 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 + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return 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 + } + + 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 := deps.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 + } + }, + ) } // 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", +func ListLabels(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + 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"}, }, - 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 + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return 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 := deps.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 + } + 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, 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), } - 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, 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), - } + response := map[string]any{ + "labels": labels, + "totalCount": int(query.Repository.Labels.TotalCount), + } - out, err := json.Marshal(response) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal labels: %w", err) - } + out, err := json.Marshal(response) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal labels: %w", err) + } - return utils.NewToolResultText(string(out)), nil, nil - }) - - return tool, handler + return utils.NewToolResultText(string(out)), nil, 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'.", +func LabelWrite(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + 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"}, }, - Required: []string{"method", "owner", "repo", "name"}, }, - } - - 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 - } - - // Get repository ID - repoID, err := getRepositoryID(ctx, client, owner, repo) - if err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find repository", err), nil, nil - } + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return 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 := deps.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 + } - input := githubv4.CreateLabelInput{ - RepositoryID: repoID, - Name: githubv4.String(name), - Color: githubv4.String(color), - } - if description != "" { - d := githubv4.String(description) - input.Description = &d - } + // Get repository ID + repoID, err := getRepositoryID(ctx, client, owner, repo) + if err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to find repository", err), nil, nil + } - var mutation struct { - CreateLabel struct { - Label struct { - Name githubv4.String - ID githubv4.ID + input := githubv4.CreateLabelInput{ + RepositoryID: repoID, + Name: githubv4.String(name), + Color: githubv4.String(color), + } + if description != "" { + d := githubv4.String(description) + input.Description = &d } - } `graphql:"createLabel(input: $input)"` - } - if err := client.Mutate(ctx, &mutation, input, nil); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to create label", err), nil, nil - } + var mutation struct { + CreateLabel struct { + Label struct { + Name githubv4.String + ID githubv4.ID + } + } `graphql:"createLabel(input: $input)"` + } - return utils.NewToolResultText(fmt.Sprintf("label '%s' created successfully", mutation.CreateLabel.Label.Name)), nil, nil + if err := client.Mutate(ctx, &mutation, input, nil); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to create label", err), 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 - } + return utils.NewToolResultText(fmt.Sprintf("label '%s' created successfully", mutation.CreateLabel.Label.Name)), nil, nil - // Get the label ID - labelID, err := getLabelID(ctx, client, owner, repo, name) - if err != nil { - return utils.NewToolResultError(err.Error()), 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 + } - 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 - } + // Get the label ID + labelID, err := getLabelID(ctx, client, owner, repo, name) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - var mutation struct { - UpdateLabel struct { - Label struct { - Name githubv4.String - ID githubv4.ID + 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 } - } `graphql:"updateLabel(input: $input)"` - } - if err := client.Mutate(ctx, &mutation, input, nil); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to update label", err), nil, nil - } + var mutation struct { + UpdateLabel struct { + Label struct { + Name githubv4.String + ID githubv4.ID + } + } `graphql:"updateLabel(input: $input)"` + } - return utils.NewToolResultText(fmt.Sprintf("label '%s' updated successfully", mutation.UpdateLabel.Label.Name)), nil, nil + if err := client.Mutate(ctx, &mutation, input, nil); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to update label", err), nil, nil + } - case "delete": - // Get the label ID - labelID, err := getLabelID(ctx, client, owner, repo, name) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + return utils.NewToolResultText(fmt.Sprintf("label '%s' updated successfully", mutation.UpdateLabel.Label.Name)), nil, nil - input := githubv4.DeleteLabelInput{ - ID: labelID, - } + case "delete": + // Get the label ID + labelID, err := getLabelID(ctx, client, owner, repo, name) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - var mutation struct { - DeleteLabel struct { - ClientMutationID githubv4.String - } `graphql:"deleteLabel(input: $input)"` - } + input := githubv4.DeleteLabelInput{ + ID: labelID, + } - if err := client.Mutate(ctx, &mutation, input, nil); err != nil { - return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to delete label", err), nil, nil - } + var mutation struct { + DeleteLabel struct { + ClientMutationID githubv4.String + } `graphql:"deleteLabel(input: $input)"` + } - return utils.NewToolResultText(fmt.Sprintf("label '%s' deleted successfully", name)), nil, nil + if err := client.Mutate(ctx, &mutation, input, nil); err != nil { + return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to delete label", err), nil, nil + } - default: - return utils.NewToolResultError(fmt.Sprintf("unknown method: %s. Supported methods are: create, update, delete", method)), nil, nil - } - }) + return utils.NewToolResultText(fmt.Sprintf("label '%s' deleted successfully", name)), nil, nil - return tool, handler + default: + return utils.NewToolResultError(fmt.Sprintf("unknown method: %s. Supported methods are: create, update, delete", method)), nil, nil + } + } + }, + ) } // Helper function to get repository ID diff --git a/pkg/github/labels_test.go b/pkg/github/labels_test.go index 12d447d72..980395ff7 100644 --- a/pkg/github/labels_test.go +++ b/pkg/github/labels_test.go @@ -17,8 +17,8 @@ func TestGetLabel(t *testing.T) { t.Parallel() // Verify tool definition - mockClient := githubv4.NewClient(nil) - tool, _ := GetLabel(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper) + serverTool := GetLabel(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_label", tool.Name) @@ -114,10 +114,13 @@ func TestGetLabel(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := githubv4.NewClient(tc.mockedClient) - _, handler := GetLabel(stubGetGQLClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetGQLClient: stubGetGQLClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) require.NoError(t, err) assert.NotNil(t, result) @@ -139,8 +142,8 @@ func TestListLabels(t *testing.T) { t.Parallel() // Verify tool definition - mockClient := githubv4.NewClient(nil) - tool, _ := ListLabels(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper) + serverTool := ListLabels(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "list_label", tool.Name) @@ -209,10 +212,13 @@ func TestListLabels(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := githubv4.NewClient(tc.mockedClient) - _, handler := ListLabels(stubGetGQLClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetGQLClient: stubGetGQLClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) require.NoError(t, err) assert.NotNil(t, result) @@ -234,8 +240,8 @@ func TestWriteLabel(t *testing.T) { t.Parallel() // Verify tool definition - mockClient := githubv4.NewClient(nil) - tool, _ := LabelWrite(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper) + serverTool := LabelWrite(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "label_write", tool.Name) @@ -457,10 +463,13 @@ func TestWriteLabel(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := githubv4.NewClient(tc.mockedClient) - _, handler := LabelWrite(stubGetGQLClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetGQLClient: stubGetGQLClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) require.NoError(t, err) assert.NotNil(t, result) diff --git a/pkg/github/projects.go b/pkg/github/projects.go index 79dfb25ce..ca26e2550 100644 --- a/pkg/github/projects.go +++ b/pkg/github/projects.go @@ -9,6 +9,7 @@ import ( "strings" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/toolsets" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" @@ -24,8 +25,9 @@ const ( MaxProjectsPerPage = 50 ) -func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func ListProjects(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "list_projects", Description: t("TOOL_LIST_PROJECTS_DESCRIPTION", `List Projects for a user or organization`), Annotations: &mcp.ToolAnnotations{ @@ -63,81 +65,87 @@ func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) ( }, Required: []string{"owner_type", "owner"}, }, - }, 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 - } + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - queryStr, err := OptionalParam[string](args, "query") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + ownerType, err := RequiredParam[string](args, "owner_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - pagination, err := extractPaginationOptionsFromArgs(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + queryStr, err := OptionalParam[string](args, "query") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + pagination, err := extractPaginationOptionsFromArgs(args) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - var resp *github.Response - var projects []*github.ProjectV2 - var queryPtr *string + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - if queryStr != "" { - queryPtr = &queryStr - } + var resp *github.Response + var projects []*github.ProjectV2 + var queryPtr *string - minimalProjects := []MinimalProject{} - opts := &github.ListProjectsOptions{ - ListProjectsPaginationOptions: pagination, - Query: queryPtr, - } + if queryStr != "" { + queryPtr = &queryStr + } - if ownerType == "org" { - projects, resp, err = client.Projects.ListOrganizationProjects(ctx, owner, opts) - } else { - projects, resp, err = client.Projects.ListUserProjects(ctx, owner, opts) - } + minimalProjects := []MinimalProject{} + opts := &github.ListProjectsOptions{ + ListProjectsPaginationOptions: pagination, + Query: queryPtr, + } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to list projects", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if ownerType == "org" { + projects, resp, err = client.Projects.ListOrganizationProjects(ctx, owner, opts) + } else { + projects, resp, err = client.Projects.ListUserProjects(ctx, owner, opts) + } - for _, project := range projects { - minimalProjects = append(minimalProjects, *convertToMinimalProject(project)) - } + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to list projects", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - response := map[string]any{ - "projects": minimalProjects, - "pageInfo": buildPageInfo(resp), - } + for _, project := range projects { + minimalProjects = append(minimalProjects, *convertToMinimalProject(project)) + } - r, err := json.Marshal(response) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + response := map[string]any{ + "projects": minimalProjects, + "pageInfo": buildPageInfo(resp), + } - return utils.NewToolResultText(string(r)), nil, nil - } + r, err := json.Marshal(response) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } -func GetProject(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func GetProject(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "get_project", Description: t("TOOL_GET_PROJECT_DESCRIPTION", "Get Project for a user or org"), Annotations: &mcp.ToolAnnotations{ @@ -163,65 +171,70 @@ func GetProject(getClient GetClientFn, t translations.TranslationHelperFunc) (mc }, Required: []string{"project_number", "owner_type", "owner"}, }, - }, func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + projectNumber, err := RequiredInt(args, "project_number") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + ownerType, err := RequiredParam[string](args, "owner_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - var resp *github.Response - var project *github.ProjectV2 + var resp *github.Response + var project *github.ProjectV2 - if ownerType == "org" { - project, resp, err = client.Projects.GetOrganizationProject(ctx, owner, projectNumber) - } else { - project, resp, err = client.Projects.GetUserProject(ctx, owner, projectNumber) - } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get project", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if ownerType == "org" { + project, resp, err = client.Projects.GetOrganizationProject(ctx, owner, projectNumber) + } else { + project, resp, err = client.Projects.GetUserProject(ctx, owner, projectNumber) + } + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get project", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get project: %s", string(body))), nil, nil + } - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) + minimalProject := convertToMinimalProject(project) + r, err := json.Marshal(minimalProject) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultError(fmt.Sprintf("failed to get project: %s", string(body))), nil, nil - } - minimalProject := convertToMinimalProject(project) - r, err := json.Marshal(minimalProject) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + return utils.NewToolResultText(string(r)), nil, nil } - - return utils.NewToolResultText(string(r)), nil, nil - } + }, + ) } -func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func ListProjectFields(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "list_project_fields", Description: t("TOOL_LIST_PROJECT_FIELDS_DESCRIPTION", "List Project fields for a user or org"), Annotations: &mcp.ToolAnnotations{ @@ -259,70 +272,76 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu }, Required: []string{"owner_type", "owner", "project_number"}, }, - }, 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 - } + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + ownerType, err := RequiredParam[string](args, "owner_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - pagination, err := extractPaginationOptionsFromArgs(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + projectNumber, err := RequiredInt(args, "project_number") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + pagination, err := extractPaginationOptionsFromArgs(args) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - var resp *github.Response - var projectFields []*github.ProjectV2Field + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - opts := &github.ListProjectsOptions{ - ListProjectsPaginationOptions: pagination, - } + var resp *github.Response + var projectFields []*github.ProjectV2Field - if ownerType == "org" { - projectFields, resp, err = client.Projects.ListOrganizationProjectFields(ctx, owner, projectNumber, opts) - } else { - projectFields, resp, err = client.Projects.ListUserProjectFields(ctx, owner, projectNumber, opts) - } + opts := &github.ListProjectsOptions{ + ListProjectsPaginationOptions: pagination, + } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to list project fields", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if ownerType == "org" { + projectFields, resp, err = client.Projects.ListOrganizationProjectFields(ctx, owner, projectNumber, opts) + } else { + projectFields, resp, err = client.Projects.ListUserProjectFields(ctx, owner, projectNumber, opts) + } - response := map[string]any{ - "fields": projectFields, - "pageInfo": buildPageInfo(resp), - } + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to list project fields", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(response) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + response := map[string]any{ + "fields": projectFields, + "pageInfo": buildPageInfo(resp), + } - return utils.NewToolResultText(string(r)), nil, nil - } + r, err := json.Marshal(response) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } -func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func GetProjectField(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "get_project_field", Description: t("TOOL_GET_PROJECT_FIELD_DESCRIPTION", "Get Project field for a user or org"), Annotations: &mcp.ToolAnnotations{ @@ -352,64 +371,70 @@ func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc }, Required: []string{"owner_type", "owner", "project_number", "field_id"}, }, - }, 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 - } - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - fieldID, err := RequiredBigInt(args, "field_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - var resp *github.Response - var projectField *github.ProjectV2Field + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + ownerType, err := RequiredParam[string](args, "owner_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + projectNumber, err := RequiredInt(args, "project_number") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + fieldID, err := RequiredBigInt(args, "field_id") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - if ownerType == "org" { - projectField, resp, err = client.Projects.GetOrganizationProjectField(ctx, owner, projectNumber, fieldID) - } else { - projectField, resp, err = client.Projects.GetUserProjectField(ctx, owner, projectNumber, fieldID) - } + var resp *github.Response + var projectField *github.ProjectV2Field - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get project field", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if ownerType == "org" { + projectField, resp, err = client.Projects.GetOrganizationProjectField(ctx, owner, projectNumber, fieldID) + } else { + projectField, resp, err = client.Projects.GetUserProjectField(ctx, owner, projectNumber, fieldID) + } - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get project field", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("failed to get project field: %s", string(body))), nil, nil + } + r, err := json.Marshal(projectField) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultError(fmt.Sprintf("failed to get project field: %s", string(body))), nil, nil - } - r, err := json.Marshal(projectField) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - return utils.NewToolResultText(string(r)), nil, nil - } + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } -func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func ListProjectItems(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "list_project_items", Description: t("TOOL_LIST_PROJECT_ITEMS_DESCRIPTION", `Search project items with advanced filtering`), Annotations: &mcp.ToolAnnotations{ @@ -458,89 +483,95 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun }, Required: []string{"owner_type", "owner", "project_number"}, }, - }, 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 - } + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + ownerType, err := RequiredParam[string](args, "owner_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - queryStr, err := OptionalParam[string](args, "query") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + projectNumber, err := RequiredInt(args, "project_number") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - fields, err := OptionalBigIntArrayParam(args, "fields") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + queryStr, err := OptionalParam[string](args, "query") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - pagination, err := extractPaginationOptionsFromArgs(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + fields, err := OptionalBigIntArrayParam(args, "fields") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + pagination, err := extractPaginationOptionsFromArgs(args) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - var resp *github.Response - var projectItems []*github.ProjectV2Item - var queryPtr *string + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - if queryStr != "" { - queryPtr = &queryStr - } + var resp *github.Response + var projectItems []*github.ProjectV2Item + var queryPtr *string - opts := &github.ListProjectItemsOptions{ - Fields: fields, - ListProjectsOptions: github.ListProjectsOptions{ - ListProjectsPaginationOptions: pagination, - Query: queryPtr, - }, - } + if queryStr != "" { + queryPtr = &queryStr + } - if ownerType == "org" { - projectItems, resp, err = client.Projects.ListOrganizationProjectItems(ctx, owner, projectNumber, opts) - } else { - projectItems, resp, err = client.Projects.ListUserProjectItems(ctx, owner, projectNumber, opts) - } + opts := &github.ListProjectItemsOptions{ + Fields: fields, + ListProjectsOptions: github.ListProjectsOptions{ + ListProjectsPaginationOptions: pagination, + Query: queryPtr, + }, + } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - ProjectListFailedError, - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if ownerType == "org" { + projectItems, resp, err = client.Projects.ListOrganizationProjectItems(ctx, owner, projectNumber, opts) + } else { + projectItems, resp, err = client.Projects.ListUserProjectItems(ctx, owner, projectNumber, opts) + } - response := map[string]any{ - "items": projectItems, - "pageInfo": buildPageInfo(resp), - } + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + ProjectListFailedError, + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(response) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + response := map[string]any{ + "items": projectItems, + "pageInfo": buildPageInfo(resp), + } - return utils.NewToolResultText(string(r)), nil, nil - } + r, err := json.Marshal(response) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } -func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func GetProjectItem(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "get_project_item", Description: t("TOOL_GET_PROJECT_ITEM_DESCRIPTION", "Get a specific Project item for a user or org"), Annotations: &mcp.ToolAnnotations{ @@ -577,71 +608,77 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) }, Required: []string{"owner_type", "owner", "project_number", "item_id"}, }, - }, 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 - } + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - itemID, err := RequiredBigInt(args, "item_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - fields, err := OptionalBigIntArrayParam(args, "fields") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + ownerType, err := RequiredParam[string](args, "owner_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + projectNumber, err := RequiredInt(args, "project_number") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + itemID, err := RequiredBigInt(args, "item_id") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + fields, err := OptionalBigIntArrayParam(args, "fields") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - var resp *github.Response - var projectItem *github.ProjectV2Item - var opts *github.GetProjectItemOptions + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - if len(fields) > 0 { - opts = &github.GetProjectItemOptions{ - Fields: fields, + var resp *github.Response + var projectItem *github.ProjectV2Item + var opts *github.GetProjectItemOptions + + if len(fields) > 0 { + opts = &github.GetProjectItemOptions{ + Fields: fields, + } } - } - if ownerType == "org" { - projectItem, resp, err = client.Projects.GetOrganizationProjectItem(ctx, owner, projectNumber, itemID, opts) - } else { - projectItem, resp, err = client.Projects.GetUserProjectItem(ctx, owner, projectNumber, itemID, opts) - } + if ownerType == "org" { + projectItem, resp, err = client.Projects.GetOrganizationProjectItem(ctx, owner, projectNumber, itemID, opts) + } else { + projectItem, resp, err = client.Projects.GetUserProjectItem(ctx, owner, projectNumber, itemID, opts) + } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get project item", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get project item", + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(projectItem) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + r, err := json.Marshal(projectItem) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - return utils.NewToolResultText(string(r)), nil, nil - } + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } -func AddProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func AddProjectItem(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "add_project_item", Description: t("TOOL_ADD_PROJECT_ITEM_DESCRIPTION", "Add a specific Project item for a user or org"), Annotations: &mcp.ToolAnnotations{ @@ -676,78 +713,84 @@ func AddProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) }, Required: []string{"owner_type", "owner", "project_number", "item_type", "item_id"}, }, - }, 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 - } - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - itemID, err := RequiredBigInt(args, "item_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - itemType, err := RequiredParam[string](args, "item_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - if itemType != "issue" && itemType != "pull_request" { - return utils.NewToolResultError("item_type must be either 'issue' or 'pull_request'"), nil, nil - } + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + ownerType, err := RequiredParam[string](args, "owner_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + projectNumber, err := RequiredInt(args, "project_number") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + itemID, err := RequiredBigInt(args, "item_id") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + itemType, err := RequiredParam[string](args, "item_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + if itemType != "issue" && itemType != "pull_request" { + return utils.NewToolResultError("item_type must be either 'issue' or 'pull_request'"), nil, nil + } - newItem := &github.AddProjectItemOptions{ - ID: itemID, - Type: toNewProjectType(itemType), - } + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - var resp *github.Response - var addedItem *github.ProjectV2Item + newItem := &github.AddProjectItemOptions{ + ID: itemID, + Type: toNewProjectType(itemType), + } - if ownerType == "org" { - addedItem, resp, err = client.Projects.AddOrganizationProjectItem(ctx, owner, projectNumber, newItem) - } else { - addedItem, resp, err = client.Projects.AddUserProjectItem(ctx, owner, projectNumber, newItem) - } + var resp *github.Response + var addedItem *github.ProjectV2Item - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - ProjectAddFailedError, - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if ownerType == "org" { + addedItem, resp, err = client.Projects.AddOrganizationProjectItem(ctx, owner, projectNumber, newItem) + } else { + addedItem, resp, err = client.Projects.AddUserProjectItem(ctx, owner, projectNumber, newItem) + } - if resp.StatusCode != http.StatusCreated { - body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return ghErrors.NewGitHubAPIErrorResponse(ctx, + ProjectAddFailedError, + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusCreated { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("%s: %s", ProjectAddFailedError, string(body))), nil, nil + } + r, err := json.Marshal(addedItem) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultError(fmt.Sprintf("%s: %s", ProjectAddFailedError, string(body))), nil, nil - } - r, err := json.Marshal(addedItem) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - return utils.NewToolResultText(string(r)), nil, nil - } + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } -func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func UpdateProjectItem(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "update_project_item", Description: t("TOOL_UPDATE_PROJECT_ITEM_DESCRIPTION", "Update a specific Project item for a user or org"), Annotations: &mcp.ToolAnnotations{ @@ -781,80 +824,86 @@ func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFu }, Required: []string{"owner_type", "owner", "project_number", "item_id", "updated_field"}, }, - }, 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 - } - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - itemID, err := RequiredBigInt(args, "item_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - rawUpdatedField, exists := args["updated_field"] - if !exists { - return utils.NewToolResultError("missing required parameter: updated_field"), nil, nil - } + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + ownerType, err := RequiredParam[string](args, "owner_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + projectNumber, err := RequiredInt(args, "project_number") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + itemID, err := RequiredBigInt(args, "item_id") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - fieldValue, ok := rawUpdatedField.(map[string]any) - if !ok || fieldValue == nil { - return utils.NewToolResultError("field_value must be an object"), nil, nil - } + rawUpdatedField, exists := args["updated_field"] + if !exists { + return utils.NewToolResultError("missing required parameter: updated_field"), nil, nil + } - updatePayload, err := buildUpdateProjectItem(fieldValue) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + fieldValue, ok := rawUpdatedField.(map[string]any) + if !ok || fieldValue == nil { + return utils.NewToolResultError("field_value must be an object"), nil, nil + } - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + updatePayload, err := buildUpdateProjectItem(fieldValue) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - var resp *github.Response - var updatedItem *github.ProjectV2Item + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - if ownerType == "org" { - updatedItem, resp, err = client.Projects.UpdateOrganizationProjectItem(ctx, owner, projectNumber, itemID, updatePayload) - } else { - updatedItem, resp, err = client.Projects.UpdateUserProjectItem(ctx, owner, projectNumber, itemID, updatePayload) - } + var resp *github.Response + var updatedItem *github.ProjectV2Item - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - ProjectUpdateFailedError, - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + if ownerType == "org" { + updatedItem, resp, err = client.Projects.UpdateOrganizationProjectItem(ctx, owner, projectNumber, itemID, updatePayload) + } else { + updatedItem, resp, err = client.Projects.UpdateUserProjectItem(ctx, owner, projectNumber, itemID, updatePayload) + } - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return ghErrors.NewGitHubAPIErrorResponse(ctx, + ProjectUpdateFailedError, + resp, + err, + ), nil, nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("%s: %s", ProjectUpdateFailedError, string(body))), nil, nil + } + r, err := json.Marshal(updatedItem) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultError(fmt.Sprintf("%s: %s", ProjectUpdateFailedError, string(body))), nil, nil - } - r, err := json.Marshal(updatedItem) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - return utils.NewToolResultText(string(r)), nil, nil - } + return utils.NewToolResultText(string(r)), nil, nil + } + }, + ) } -func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { - return mcp.Tool{ +func DeleteProjectItem(t translations.TranslationHelperFunc) toolsets.ServerTool { + return NewTool( + mcp.Tool{ Name: "delete_project_item", Description: t("TOOL_DELETE_PROJECT_ITEM_DESCRIPTION", "Delete a specific Project item for a user or org"), Annotations: &mcp.ToolAnnotations{ @@ -884,53 +933,58 @@ func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFu }, Required: []string{"owner_type", "owner", "project_number", "item_id"}, }, - }, 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 - } - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - itemID, err := RequiredBigInt(args, "item_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - client, err := getClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + }, + func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - var resp *github.Response - if ownerType == "org" { - resp, err = client.Projects.DeleteOrganizationProjectItem(ctx, owner, projectNumber, itemID) - } else { - resp, err = client.Projects.DeleteUserProjectItem(ctx, owner, projectNumber, itemID) - } + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + ownerType, err := RequiredParam[string](args, "owner_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + projectNumber, err := RequiredInt(args, "project_number") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + itemID, err := RequiredBigInt(args, "item_id") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - ProjectDeleteFailedError, - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() + var resp *github.Response + if ownerType == "org" { + resp, err = client.Projects.DeleteOrganizationProjectItem(ctx, owner, projectNumber, itemID) + } else { + resp, err = client.Projects.DeleteUserProjectItem(ctx, owner, projectNumber, itemID) + } - if resp.StatusCode != http.StatusNoContent { - body, err := io.ReadAll(resp.Body) if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) + return ghErrors.NewGitHubAPIErrorResponse(ctx, + ProjectDeleteFailedError, + resp, + err, + ), nil, nil } - return utils.NewToolResultError(fmt.Sprintf("%s: %s", ProjectDeleteFailedError, string(body))), nil, nil + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusNoContent { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + return utils.NewToolResultError(fmt.Sprintf("%s: %s", ProjectDeleteFailedError, string(body))), nil, nil + } + return utils.NewToolResultText("project item successfully deleted"), nil, nil } - return utils.NewToolResultText("project item successfully deleted"), nil, nil - } + }, + ) } type pageInfo struct { diff --git a/pkg/github/projects_test.go b/pkg/github/projects_test.go index e2814c8f9..0c2e2ab52 100644 --- a/pkg/github/projects_test.go +++ b/pkg/github/projects_test.go @@ -17,8 +17,8 @@ import ( ) func Test_ListProjects(t *testing.T) { - mockClient := gh.NewClient(nil) - tool, _ := ListProjects(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := ListProjects(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "list_projects", tool.Name) @@ -141,9 +141,12 @@ func Test_ListProjects(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := gh.NewClient(tc.mockedClient) - _, handler := ListProjects(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) require.NoError(t, err) if tc.expectError { @@ -177,8 +180,8 @@ func Test_ListProjects(t *testing.T) { } func Test_GetProject(t *testing.T) { - mockClient := gh.NewClient(nil) - tool, _ := GetProject(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := GetProject(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_project", tool.Name) @@ -277,9 +280,12 @@ func Test_GetProject(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := gh.NewClient(tc.mockedClient) - _, handler := GetProject(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) require.NoError(t, err) if tc.expectError { @@ -310,8 +316,8 @@ func Test_GetProject(t *testing.T) { } func Test_ListProjectFields(t *testing.T) { - mockClient := gh.NewClient(nil) - tool, _ := ListProjectFields(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := ListProjectFields(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "list_project_fields", tool.Name) @@ -426,9 +432,12 @@ func Test_ListProjectFields(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := gh.NewClient(tc.mockedClient) - _, handler := ListProjectFields(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) require.NoError(t, err) if tc.expectError { @@ -464,8 +473,8 @@ func Test_ListProjectFields(t *testing.T) { } func Test_GetProjectField(t *testing.T) { - mockClient := gh.NewClient(nil) - tool, _ := GetProjectField(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := GetProjectField(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_project_field", tool.Name) @@ -583,9 +592,12 @@ func Test_GetProjectField(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := gh.NewClient(tc.mockedClient) - _, handler := GetProjectField(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) require.NoError(t, err) if tc.expectError { @@ -622,8 +634,8 @@ func Test_GetProjectField(t *testing.T) { } func Test_ListProjectItems(t *testing.T) { - mockClient := gh.NewClient(nil) - tool, _ := ListProjectItems(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := ListProjectItems(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "list_project_items", tool.Name) @@ -786,9 +798,12 @@ func Test_ListProjectItems(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := gh.NewClient(tc.mockedClient) - _, handler := ListProjectItems(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) require.NoError(t, err) if tc.expectError { @@ -824,8 +839,8 @@ func Test_ListProjectItems(t *testing.T) { } func Test_GetProjectItem(t *testing.T) { - mockClient := gh.NewClient(nil) - tool, _ := GetProjectItem(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := GetProjectItem(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "get_project_item", tool.Name) @@ -980,9 +995,12 @@ func Test_GetProjectItem(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := gh.NewClient(tc.mockedClient) - _, handler := GetProjectItem(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) require.NoError(t, err) if tc.expectError { @@ -1019,8 +1037,8 @@ func Test_GetProjectItem(t *testing.T) { } func Test_AddProjectItem(t *testing.T) { - mockClient := gh.NewClient(nil) - tool, _ := AddProjectItem(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := AddProjectItem(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "add_project_item", tool.Name) @@ -1206,10 +1224,13 @@ func Test_AddProjectItem(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := gh.NewClient(tc.mockedClient) - _, handler := AddProjectItem(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) require.NoError(t, err) if tc.expectError { @@ -1255,8 +1276,8 @@ func Test_AddProjectItem(t *testing.T) { } func Test_UpdateProjectItem(t *testing.T) { - mockClient := gh.NewClient(nil) - tool, _ := UpdateProjectItem(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := UpdateProjectItem(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "update_project_item", tool.Name) @@ -1488,9 +1509,12 @@ func Test_UpdateProjectItem(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := gh.NewClient(tc.mockedClient) - _, handler := UpdateProjectItem(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) require.NoError(t, err) if tc.expectError { @@ -1532,8 +1556,8 @@ func Test_UpdateProjectItem(t *testing.T) { } func Test_DeleteProjectItem(t *testing.T) { - mockClient := gh.NewClient(nil) - tool, _ := DeleteProjectItem(stubGetClientFn(mockClient), translations.NullTranslationHelper) + serverTool := DeleteProjectItem(translations.NullTranslationHelper) + tool := serverTool.Tool require.NoError(t, toolsnaps.Test(tool.Name, tool)) assert.Equal(t, "delete_project_item", tool.Name) @@ -1652,9 +1676,12 @@ func Test_DeleteProjectItem(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := gh.NewClient(tc.mockedClient) - _, handler := DeleteProjectItem(stubGetClientFn(client), translations.NullTranslationHelper) + deps := ToolDependencies{ + GetClient: stubGetClientFn(client), + } + handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) - result, _, err := handler(context.Background(), &request, tc.requestArgs) + result, err := handler(context.Background(), &request) require.NoError(t, err) if tc.expectError { diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 6af9ce40d..02ec66e8a 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -218,7 +218,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG SearchIssues(t), ListIssues(t), ListIssueTypes(t), - toolsets.NewServerToolLegacy(GetLabel(getGQLClient, t)), + GetLabel(t), ). AddWriteTools( IssueWrite(t), @@ -353,17 +353,17 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG projects := toolsets.NewToolset(ToolsetMetadataProjects.ID, ToolsetMetadataProjects.Description). SetDependencies(deps). AddReadTools( - toolsets.NewServerToolLegacy(ListProjects(getClient, t)), - toolsets.NewServerToolLegacy(GetProject(getClient, t)), - toolsets.NewServerToolLegacy(ListProjectFields(getClient, t)), - toolsets.NewServerToolLegacy(GetProjectField(getClient, t)), - toolsets.NewServerToolLegacy(ListProjectItems(getClient, t)), - toolsets.NewServerToolLegacy(GetProjectItem(getClient, t)), + ListProjects(t), + GetProject(t), + ListProjectFields(t), + GetProjectField(t), + ListProjectItems(t), + GetProjectItem(t), ). AddWriteTools( - toolsets.NewServerToolLegacy(AddProjectItem(getClient, t)), - toolsets.NewServerToolLegacy(DeleteProjectItem(getClient, t)), - toolsets.NewServerToolLegacy(UpdateProjectItem(getClient, t)), + AddProjectItem(t), + DeleteProjectItem(t), + UpdateProjectItem(t), ) stargazers := toolsets.NewToolset(ToolsetMetadataStargazers.ID, ToolsetMetadataStargazers.Description). SetDependencies(deps). @@ -378,13 +378,13 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG SetDependencies(deps). AddReadTools( // get - toolsets.NewServerToolLegacy(GetLabel(getGQLClient, t)), + GetLabel(t), // list labels on repo or issue - toolsets.NewServerToolLegacy(ListLabels(getGQLClient, t)), + ListLabels(t), ). AddWriteTools( // create or update - toolsets.NewServerToolLegacy(LabelWrite(getGQLClient, t)), + LabelWrite(t), ) // Add toolsets to the group @@ -421,9 +421,9 @@ func InitDynamicToolset(s *mcp.Server, tsg *toolsets.ToolsetGroup, t translation // Need to add the dynamic toolset last so it can be used to enable other toolsets dynamicToolSelection := toolsets.NewToolset(ToolsetMetadataDynamic.ID, ToolsetMetadataDynamic.Description). AddReadTools( - toolsets.NewServerToolLegacy(ListAvailableToolsets(tsg, t)), - toolsets.NewServerToolLegacy(GetToolsetsTools(tsg, t)), - toolsets.NewServerToolLegacy(EnableToolset(s, tsg, t)), + ListAvailableToolsets(tsg, t), + GetToolsetsTools(tsg, t), + EnableToolset(s, tsg, t), ) dynamicToolSelection.Enabled = true