diff --git a/README.md b/README.md index 1e37bc0e1..ecd24ba2d 100644 --- a/README.md +++ b/README.md @@ -404,6 +404,7 @@ The following sets of tools are available: | `labels` | GitHub Labels related tools | | `notifications` | GitHub Notifications related tools | | `orgs` | GitHub Organization related tools | +| `packages` | GitHub Packages related tools for managing and viewing package metadata, versions, and deletion operations | | `projects` | GitHub Projects related tools | | `pull_requests` | GitHub Pull Request related tools | | `repos` | GitHub Repository related tools | @@ -793,6 +794,65 @@ Options are:
+Packages + +- **delete_org_package** - Delete organization package + - `org`: Organization name (string, required) + - `package_name`: Package name (string, required) + - `package_type`: Package type (string, required) + +- **delete_org_package_version** - Delete organization package version + - `org`: Organization name (string, required) + - `package_name`: Package name (string, required) + - `package_type`: Package type (string, required) + - `package_version_id`: Package version ID (number, required) + +- **delete_user_package** - Delete user package + - `package_name`: Package name (string, required) + - `package_type`: Package type (string, required) + +- **delete_user_package_version** - Delete user package version + - `package_name`: Package name (string, required) + - `package_type`: Package type (string, required) + - `package_version_id`: Package version ID (number, required) + +- **get_org_package** - Get organization package details + - `org`: Organization name (string, required) + - `package_name`: Package name (string, required) + - `package_type`: Package type (string, required) + +- **get_package_version** - Get package version details + - `org`: Organization name (string, required) + - `package_name`: Package name (string, required) + - `package_type`: Package type (string, required) + - `package_version_id`: Package version ID (number, required) + +- **list_org_packages** - List organization packages + - `org`: Organization name (string, required) + - `package_type`: Filter by package type (string, optional) + - `page`: Page number for pagination (min 1) (number, optional) + - `perPage`: Results per page for pagination (min 1, max 100) (number, optional) + - `visibility`: Filter by package visibility (string, optional) + +- **list_package_versions** - List package versions + - `org`: Organization name (string, required) + - `package_name`: Package name (string, required) + - `package_type`: Package type (string, required) + - `page`: Page number for pagination (min 1) (number, optional) + - `perPage`: Results per page for pagination (min 1, max 100) (number, optional) + - `state`: Filter by version state (string, optional) + +- **list_user_packages** - List user packages + - `package_type`: Filter by package type (string, optional) + - `page`: Page number for pagination (min 1) (number, optional) + - `perPage`: Results per page for pagination (min 1, max 100) (number, optional) + - `username`: GitHub username (string, required) + - `visibility`: Filter by package visibility (string, optional) + +
+ +
+ Projects - **add_project_item** - Add project item diff --git a/docs/remote-server.md b/docs/remote-server.md index fa55168e5..805307741 100644 --- a/docs/remote-server.md +++ b/docs/remote-server.md @@ -30,6 +30,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to | Labels | GitHub Labels related tools | https://api.githubcopilot.com/mcp/x/labels | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-labels&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Flabels%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/labels/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-labels&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Flabels%2Freadonly%22%7D) | | Notifications | GitHub Notifications related tools | https://api.githubcopilot.com/mcp/x/notifications | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/notifications/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%2Freadonly%22%7D) | | Organizations | GitHub Organization related tools | https://api.githubcopilot.com/mcp/x/orgs | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-orgs&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Forgs%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/orgs/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-orgs&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Forgs%2Freadonly%22%7D) | +| Packages | GitHub Packages related tools for managing and viewing package metadata, versions, and deletion operations | https://api.githubcopilot.com/mcp/x/packages | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-packages&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fpackages%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/packages/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-packages&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fpackages%2Freadonly%22%7D) | | Projects | GitHub Projects related tools | https://api.githubcopilot.com/mcp/x/projects | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-projects&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fprojects%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/projects/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-projects&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fprojects%2Freadonly%22%7D) | | Pull Requests | GitHub Pull Request related tools | https://api.githubcopilot.com/mcp/x/pull_requests | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-pull_requests&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fpull_requests%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/pull_requests/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-pull_requests&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fpull_requests%2Freadonly%22%7D) | | Repositories | GitHub Repository related tools | https://api.githubcopilot.com/mcp/x/repos | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-repos&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Frepos%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/repos/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-repos&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Frepos%2Freadonly%22%7D) | diff --git a/pkg/github/__toolsnaps__/delete_org_package.snap b/pkg/github/__toolsnaps__/delete_org_package.snap new file mode 100644 index 000000000..5c2021c0c --- /dev/null +++ b/pkg/github/__toolsnaps__/delete_org_package.snap @@ -0,0 +1,38 @@ +{ + "annotations": { + "title": "Delete organization package", + "readOnlyHint": false + }, + "description": "Delete an entire package from a GitHub organization. This will delete all versions of the package. Requires delete:packages scope.", + "inputSchema": { + "properties": { + "org": { + "description": "Organization name", + "type": "string" + }, + "package_name": { + "description": "Package name", + "type": "string" + }, + "package_type": { + "description": "Package type", + "enum": [ + "npm", + "maven", + "rubygems", + "docker", + "nuget", + "container" + ], + "type": "string" + } + }, + "required": [ + "org", + "package_type", + "package_name" + ], + "type": "object" + }, + "name": "delete_org_package" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/delete_org_package_version.snap b/pkg/github/__toolsnaps__/delete_org_package_version.snap new file mode 100644 index 000000000..c1379bc3f --- /dev/null +++ b/pkg/github/__toolsnaps__/delete_org_package_version.snap @@ -0,0 +1,43 @@ +{ + "annotations": { + "title": "Delete organization package version", + "readOnlyHint": false + }, + "description": "Delete a specific version of a package from a GitHub organization. Requires delete:packages scope.", + "inputSchema": { + "properties": { + "org": { + "description": "Organization name", + "type": "string" + }, + "package_name": { + "description": "Package name", + "type": "string" + }, + "package_type": { + "description": "Package type", + "enum": [ + "npm", + "maven", + "rubygems", + "docker", + "nuget", + "container" + ], + "type": "string" + }, + "package_version_id": { + "description": "Package version ID", + "type": "number" + } + }, + "required": [ + "org", + "package_type", + "package_name", + "package_version_id" + ], + "type": "object" + }, + "name": "delete_org_package_version" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/delete_user_package.snap b/pkg/github/__toolsnaps__/delete_user_package.snap new file mode 100644 index 000000000..42d526f2a --- /dev/null +++ b/pkg/github/__toolsnaps__/delete_user_package.snap @@ -0,0 +1,33 @@ +{ + "annotations": { + "title": "Delete user package", + "readOnlyHint": false + }, + "description": "Delete an entire package from the authenticated user's account. This will delete all versions of the package. Requires delete:packages scope.", + "inputSchema": { + "properties": { + "package_name": { + "description": "Package name", + "type": "string" + }, + "package_type": { + "description": "Package type", + "enum": [ + "npm", + "maven", + "rubygems", + "docker", + "nuget", + "container" + ], + "type": "string" + } + }, + "required": [ + "package_type", + "package_name" + ], + "type": "object" + }, + "name": "delete_user_package" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/delete_user_package_version.snap b/pkg/github/__toolsnaps__/delete_user_package_version.snap new file mode 100644 index 000000000..d12f2829e --- /dev/null +++ b/pkg/github/__toolsnaps__/delete_user_package_version.snap @@ -0,0 +1,38 @@ +{ + "annotations": { + "title": "Delete user package version", + "readOnlyHint": false + }, + "description": "Delete a specific version of a package from the authenticated user's account. Requires delete:packages scope.", + "inputSchema": { + "properties": { + "package_name": { + "description": "Package name", + "type": "string" + }, + "package_type": { + "description": "Package type", + "enum": [ + "npm", + "maven", + "rubygems", + "docker", + "nuget", + "container" + ], + "type": "string" + }, + "package_version_id": { + "description": "Package version ID", + "type": "number" + } + }, + "required": [ + "package_type", + "package_name", + "package_version_id" + ], + "type": "object" + }, + "name": "delete_user_package_version" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_org_package.snap b/pkg/github/__toolsnaps__/get_org_package.snap new file mode 100644 index 000000000..70c8b5027 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_org_package.snap @@ -0,0 +1,38 @@ +{ + "annotations": { + "title": "Get organization package details", + "readOnlyHint": true + }, + "description": "Get details of a specific package for an organization.", + "inputSchema": { + "properties": { + "org": { + "description": "Organization name", + "type": "string" + }, + "package_name": { + "description": "Package name", + "type": "string" + }, + "package_type": { + "description": "Package type", + "enum": [ + "npm", + "maven", + "rubygems", + "docker", + "nuget", + "container" + ], + "type": "string" + } + }, + "required": [ + "org", + "package_type", + "package_name" + ], + "type": "object" + }, + "name": "get_org_package" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_package_version.snap b/pkg/github/__toolsnaps__/get_package_version.snap new file mode 100644 index 000000000..67d841d15 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_package_version.snap @@ -0,0 +1,43 @@ +{ + "annotations": { + "title": "Get package version details", + "readOnlyHint": true + }, + "description": "Get details of a specific package version, including metadata.", + "inputSchema": { + "properties": { + "org": { + "description": "Organization name", + "type": "string" + }, + "package_name": { + "description": "Package name", + "type": "string" + }, + "package_type": { + "description": "Package type", + "enum": [ + "npm", + "maven", + "rubygems", + "docker", + "nuget", + "container" + ], + "type": "string" + }, + "package_version_id": { + "description": "Package version ID", + "type": "number" + } + }, + "required": [ + "org", + "package_type", + "package_name", + "package_version_id" + ], + "type": "object" + }, + "name": "get_package_version" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_org_packages.snap b/pkg/github/__toolsnaps__/list_org_packages.snap new file mode 100644 index 000000000..669d69100 --- /dev/null +++ b/pkg/github/__toolsnaps__/list_org_packages.snap @@ -0,0 +1,52 @@ +{ + "annotations": { + "title": "List organization packages", + "readOnlyHint": true + }, + "description": "List packages for a GitHub organization. Returns package metadata including name, type, visibility, and version count.", + "inputSchema": { + "properties": { + "org": { + "description": "Organization name", + "type": "string" + }, + "package_type": { + "description": "Filter by package type", + "enum": [ + "npm", + "maven", + "rubygems", + "docker", + "nuget", + "container" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "visibility": { + "description": "Filter by package visibility", + "enum": [ + "public", + "private", + "internal" + ], + "type": "string" + } + }, + "required": [ + "org" + ], + "type": "object" + }, + "name": "list_org_packages" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_package_versions.snap b/pkg/github/__toolsnaps__/list_package_versions.snap new file mode 100644 index 000000000..c5f51d2cb --- /dev/null +++ b/pkg/github/__toolsnaps__/list_package_versions.snap @@ -0,0 +1,57 @@ +{ + "annotations": { + "title": "List package versions", + "readOnlyHint": true + }, + "description": "List versions of a package for an organization. Each version includes metadata.", + "inputSchema": { + "properties": { + "org": { + "description": "Organization name", + "type": "string" + }, + "package_name": { + "description": "Package name", + "type": "string" + }, + "package_type": { + "description": "Package type", + "enum": [ + "npm", + "maven", + "rubygems", + "docker", + "nuget", + "container" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "state": { + "description": "Filter by version state", + "enum": [ + "active", + "deleted" + ], + "type": "string" + } + }, + "required": [ + "org", + "package_type", + "package_name" + ], + "type": "object" + }, + "name": "list_package_versions" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_user_packages.snap b/pkg/github/__toolsnaps__/list_user_packages.snap new file mode 100644 index 000000000..f85a06f27 --- /dev/null +++ b/pkg/github/__toolsnaps__/list_user_packages.snap @@ -0,0 +1,52 @@ +{ + "annotations": { + "title": "List user packages", + "readOnlyHint": true + }, + "description": "List packages for a GitHub user. Note: Download statistics are not available via the GitHub REST API.", + "inputSchema": { + "properties": { + "package_type": { + "description": "Filter by package type", + "enum": [ + "npm", + "maven", + "rubygems", + "docker", + "nuget", + "container" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "username": { + "description": "GitHub username", + "type": "string" + }, + "visibility": { + "description": "Filter by package visibility", + "enum": [ + "public", + "private", + "internal" + ], + "type": "string" + } + }, + "required": [ + "username" + ], + "type": "object" + }, + "name": "list_user_packages" +} \ No newline at end of file diff --git a/pkg/github/packages.go b/pkg/github/packages.go new file mode 100644 index 000000000..23e83649f --- /dev/null +++ b/pkg/github/packages.go @@ -0,0 +1,641 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v74/github" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +// authenticatedUser represents the empty string used to indicate operations +// should be performed on the authenticated user's account rather than a specific username. +// This is used by GitHub's API when the username parameter is empty. +const authenticatedUser = "" + +// NOTE: GitHub's REST API for packages does not currently expose download statistics. +// While download counts are visible on the GitHub web interface (e.g., github.com/orgs/{org}/packages), +// they are not included in the API responses. + +// handleDeletionResponse handles the common response logic for package deletion operations. +// It checks the status code, reads error messages if any, and returns a standardized success response. +func handleDeletionResponse(resp *github.Response, successMessage string) (*mcp.CallToolResult, error) { + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusNoContent { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return mcp.NewToolResultError(fmt.Sprintf("deletion failed: %s", string(body))), nil + } + + result := map[string]interface{}{ + "success": true, + "message": successMessage, + } + + r, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil +} + +// ListOrgPackages creates a tool to list packages for an organization +func ListOrgPackages(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_org_packages", + mcp.WithDescription(t("TOOL_LIST_ORG_PACKAGES_DESCRIPTION", "List packages for a GitHub organization. Returns package metadata including name, type, visibility, and version count.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_LIST_ORG_PACKAGES_USER_TITLE", "List organization packages"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("org", + mcp.Required(), + mcp.Description("Organization name"), + ), + mcp.WithString("package_type", + mcp.Description("Filter by package type"), + mcp.Enum("npm", "maven", "rubygems", "docker", "nuget", "container"), + ), + mcp.WithString("visibility", + mcp.Description("Filter by package visibility"), + mcp.Enum("public", "private", "internal"), + ), + WithPagination(), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + org, err := RequiredParam[string](request, "org") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageType, err := OptionalParam[string](request, "package_type") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + visibility, err := OptionalParam[string](request, "visibility") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + pagination, err := OptionalPaginationParams(request) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + opts := &github.PackageListOptions{ + ListOptions: github.ListOptions{ + Page: pagination.Page, + PerPage: pagination.PerPage, + }, + } + + // Only set optional parameters if they have values + if packageType != "" { + opts.PackageType = github.Ptr(packageType) + } + if visibility != "" { + opts.Visibility = github.Ptr(visibility) + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + packages, resp, err := client.Organizations.ListPackages(ctx, org, opts) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to list packages for organization '%s'", org), + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + r, err := json.Marshal(packages) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// GetOrgPackage creates a tool to get a specific package for an organization with download statistics +func GetOrgPackage(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_org_package", + mcp.WithDescription(t("TOOL_GET_ORG_PACKAGE_DESCRIPTION", "Get details of a specific package for an organization.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_GET_ORG_PACKAGE_USER_TITLE", "Get organization package details"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("org", + mcp.Required(), + mcp.Description("Organization name"), + ), + mcp.WithString("package_type", + mcp.Required(), + mcp.Description("Package type"), + mcp.Enum("npm", "maven", "rubygems", "docker", "nuget", "container"), + ), + mcp.WithString("package_name", + mcp.Required(), + mcp.Description("Package name"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + org, err := RequiredParam[string](request, "org") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageType, err := RequiredParam[string](request, "package_type") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageName, err := RequiredParam[string](request, "package_name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + pkg, resp, err := client.Organizations.GetPackage(ctx, org, packageType, packageName) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to get package '%s' of type '%s' for organization '%s'", packageName, packageType, org), + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + r, err := json.Marshal(pkg) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// ListPackageVersions creates a tool to list versions of a package with download statistics +func ListPackageVersions(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_package_versions", + mcp.WithDescription(t("TOOL_LIST_PACKAGE_VERSIONS_DESCRIPTION", "List versions of a package for an organization. Each version includes metadata.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_LIST_PACKAGE_VERSIONS_USER_TITLE", "List package versions"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("org", + mcp.Required(), + mcp.Description("Organization name"), + ), + mcp.WithString("package_type", + mcp.Required(), + mcp.Description("Package type"), + mcp.Enum("npm", "maven", "rubygems", "docker", "nuget", "container"), + ), + mcp.WithString("package_name", + mcp.Required(), + mcp.Description("Package name"), + ), + mcp.WithString("state", + mcp.Description("Filter by version state"), + mcp.Enum("active", "deleted"), + ), + WithPagination(), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + org, err := RequiredParam[string](request, "org") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageType, err := RequiredParam[string](request, "package_type") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageName, err := RequiredParam[string](request, "package_name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + state, err := OptionalParam[string](request, "state") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + pagination, err := OptionalPaginationParams(request) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + opts := &github.PackageListOptions{ + ListOptions: github.ListOptions{ + Page: pagination.Page, + PerPage: pagination.PerPage, + }, + } + + // Only set state parameter if it has a value + if state != "" { + opts.State = github.Ptr(state) + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + versions, resp, err := client.Organizations.PackageGetAllVersions(ctx, org, packageType, packageName, opts) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to list package versions for package '%s' of type '%s'", packageName, packageType), + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + r, err := json.Marshal(versions) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// GetPackageVersion creates a tool to get a specific package version with download statistics +func GetPackageVersion(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_package_version", + mcp.WithDescription(t("TOOL_GET_PACKAGE_VERSION_DESCRIPTION", "Get details of a specific package version, including metadata.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_GET_PACKAGE_VERSION_USER_TITLE", "Get package version details"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("org", + mcp.Required(), + mcp.Description("Organization name"), + ), + mcp.WithString("package_type", + mcp.Required(), + mcp.Description("Package type"), + mcp.Enum("npm", "maven", "rubygems", "docker", "nuget", "container"), + ), + mcp.WithString("package_name", + mcp.Required(), + mcp.Description("Package name"), + ), + mcp.WithNumber("package_version_id", + mcp.Required(), + mcp.Description("Package version ID"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + org, err := RequiredParam[string](request, "org") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageType, err := RequiredParam[string](request, "package_type") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageName, err := RequiredParam[string](request, "package_name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageVersionID, err := RequiredParam[float64](request, "package_version_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + version, resp, err := client.Organizations.PackageGetVersion(ctx, org, packageType, packageName, int64(packageVersionID)) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to get package version %d for package '%s'", int64(packageVersionID), packageName), + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + r, err := json.Marshal(version) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// ListUserPackages creates a tool to list packages for a user +func ListUserPackages(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_user_packages", + mcp.WithDescription(t("TOOL_LIST_USER_PACKAGES_DESCRIPTION", "List packages for a GitHub user. Note: Download statistics are not available via the GitHub REST API.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_LIST_USER_PACKAGES_USER_TITLE", "List user packages"), + ReadOnlyHint: ToBoolPtr(true), + }), + mcp.WithString("username", + mcp.Required(), + mcp.Description("GitHub username"), + ), + mcp.WithString("package_type", + mcp.Description("Filter by package type"), + mcp.Enum("npm", "maven", "rubygems", "docker", "nuget", "container"), + ), + mcp.WithString("visibility", + mcp.Description("Filter by package visibility"), + mcp.Enum("public", "private", "internal"), + ), + WithPagination(), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + username, err := RequiredParam[string](request, "username") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageType, err := OptionalParam[string](request, "package_type") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + visibility, err := OptionalParam[string](request, "visibility") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + pagination, err := OptionalPaginationParams(request) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + opts := &github.PackageListOptions{ + ListOptions: github.ListOptions{ + Page: pagination.Page, + PerPage: pagination.PerPage, + }, + } + + // Only set optional parameters if they have values + if packageType != "" { + opts.PackageType = github.Ptr(packageType) + } + if visibility != "" { + opts.Visibility = github.Ptr(visibility) + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + packages, resp, err := client.Users.ListPackages(ctx, username, opts) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to list packages for user '%s'", username), + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + + r, err := json.Marshal(packages) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +// DeleteOrgPackage creates a tool to delete an entire package from an organization +// Requires delete:packages scope in addition to read:packages +func DeleteOrgPackage(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("delete_org_package", + mcp.WithDescription(t("TOOL_DELETE_ORG_PACKAGE_DESCRIPTION", "Delete an entire package from a GitHub organization. This will delete all versions of the package. Requires delete:packages scope.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_DELETE_ORG_PACKAGE_USER_TITLE", "Delete organization package"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("org", + mcp.Required(), + mcp.Description("Organization name"), + ), + mcp.WithString("package_type", + mcp.Required(), + mcp.Description("Package type"), + mcp.Enum("npm", "maven", "rubygems", "docker", "nuget", "container"), + ), + mcp.WithString("package_name", + mcp.Required(), + mcp.Description("Package name"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + org, err := RequiredParam[string](request, "org") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageType, err := RequiredParam[string](request, "package_type") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageName, err := RequiredParam[string](request, "package_name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + resp, err := client.Organizations.DeletePackage(ctx, org, packageType, packageName) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to delete package '%s' of type '%s' for organization '%s'", packageName, packageType, org), + resp, + err, + ), nil + } + + return handleDeletionResponse(resp, fmt.Sprintf("Package '%s' deleted successfully from organization '%s'", packageName, org)) + } +} + +// DeleteOrgPackageVersion creates a tool to delete a specific version of a package from an organization +// Requires delete:packages scope in addition to read:packages +func DeleteOrgPackageVersion(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("delete_org_package_version", + mcp.WithDescription(t("TOOL_DELETE_ORG_PACKAGE_VERSION_DESCRIPTION", "Delete a specific version of a package from a GitHub organization. Requires delete:packages scope.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_DELETE_ORG_PACKAGE_VERSION_USER_TITLE", "Delete organization package version"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("org", + mcp.Required(), + mcp.Description("Organization name"), + ), + mcp.WithString("package_type", + mcp.Required(), + mcp.Description("Package type"), + mcp.Enum("npm", "maven", "rubygems", "docker", "nuget", "container"), + ), + mcp.WithString("package_name", + mcp.Required(), + mcp.Description("Package name"), + ), + mcp.WithNumber("package_version_id", + mcp.Required(), + mcp.Description("Package version ID"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + org, err := RequiredParam[string](request, "org") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageType, err := RequiredParam[string](request, "package_type") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageName, err := RequiredParam[string](request, "package_name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageVersionID, err := RequiredParam[float64](request, "package_version_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + resp, err := client.Organizations.PackageDeleteVersion(ctx, org, packageType, packageName, int64(packageVersionID)) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to delete package version %d of package '%s'", int64(packageVersionID), packageName), + resp, + err, + ), nil + } + + return handleDeletionResponse(resp, fmt.Sprintf("Package version %d deleted successfully from package '%s'", int64(packageVersionID), packageName)) + } +} + +// DeleteUserPackage creates a tool to delete an entire package from the authenticated user's account +// Requires delete:packages scope in addition to read:packages +func DeleteUserPackage(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("delete_user_package", + mcp.WithDescription(t("TOOL_DELETE_USER_PACKAGE_DESCRIPTION", "Delete an entire package from the authenticated user's account. This will delete all versions of the package. Requires delete:packages scope.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_DELETE_USER_PACKAGE_USER_TITLE", "Delete user package"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("package_type", + mcp.Required(), + mcp.Description("Package type"), + mcp.Enum("npm", "maven", "rubygems", "docker", "nuget", "container"), + ), + mcp.WithString("package_name", + mcp.Required(), + mcp.Description("Package name"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + packageType, err := RequiredParam[string](request, "package_type") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageName, err := RequiredParam[string](request, "package_name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + resp, err := client.Users.DeletePackage(ctx, authenticatedUser, packageType, packageName) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to delete package '%s' of type '%s'", packageName, packageType), + resp, + err, + ), nil + } + + return handleDeletionResponse(resp, fmt.Sprintf("Package '%s' deleted successfully", packageName)) + } +} + +// DeleteUserPackageVersion creates a tool to delete a specific version of a package from the authenticated user's account +// Requires delete:packages scope in addition to read:packages +func DeleteUserPackageVersion(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("delete_user_package_version", + mcp.WithDescription(t("TOOL_DELETE_USER_PACKAGE_VERSION_DESCRIPTION", "Delete a specific version of a package from the authenticated user's account. Requires delete:packages scope.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_DELETE_USER_PACKAGE_VERSION_USER_TITLE", "Delete user package version"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("package_type", + mcp.Required(), + mcp.Description("Package type"), + mcp.Enum("npm", "maven", "rubygems", "docker", "nuget", "container"), + ), + mcp.WithString("package_name", + mcp.Required(), + mcp.Description("Package name"), + ), + mcp.WithNumber("package_version_id", + mcp.Required(), + mcp.Description("Package version ID"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + packageType, err := RequiredParam[string](request, "package_type") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageName, err := RequiredParam[string](request, "package_name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + packageVersionID, err := RequiredParam[float64](request, "package_version_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + resp, err := client.Users.PackageDeleteVersion(ctx, authenticatedUser, packageType, packageName, int64(packageVersionID)) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + fmt.Sprintf("failed to delete version %d of package '%s'", int64(packageVersionID), packageName), + resp, + err, + ), nil + } + + return handleDeletionResponse(resp, fmt.Sprintf("Package version %d deleted successfully from package '%s'", int64(packageVersionID), packageName)) + } +} diff --git a/pkg/github/packages_test.go b/pkg/github/packages_test.go new file mode 100644 index 000000000..ab9e3755d --- /dev/null +++ b/pkg/github/packages_test.go @@ -0,0 +1,1005 @@ +package github + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/github/github-mcp-server/internal/toolsnaps" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v74/github" + "github.com/mark3labs/mcp-go/mcp" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// verifyDeletionSuccess is a helper function to verify deletion operation success. +// It checks that the result is not an error, parses the JSON response, and verifies +// the success status and message content. +func verifyDeletionSuccess(t *testing.T, result *mcp.CallToolResult, err error) { + t.Helper() + + require.NoError(t, err) + require.False(t, result.IsError) + + // Parse the success result + textContent := getTextResult(t, result) + var response map[string]interface{} + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + + assert.True(t, response["success"].(bool)) + assert.Contains(t, response["message"].(string), "deleted successfully") +} + +func Test_ListOrgPackages(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := ListOrgPackages(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "list_org_packages", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "org") + assert.Contains(t, tool.InputSchema.Properties, "package_type") + assert.Contains(t, tool.InputSchema.Properties, "visibility") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"org"}) + + // Setup mock packages for success case + mockPackages := []*github.Package{ + { + ID: github.Ptr(int64(1)), + Name: github.Ptr("github-mcp-server"), + PackageType: github.Ptr("container"), + HTMLURL: github.Ptr("https://github.com/orgs/github/packages/container/package/github-mcp-server"), + Visibility: github.Ptr("public"), + }, + { + ID: github.Ptr(int64(2)), + Name: github.Ptr("test-package"), + PackageType: github.Ptr("npm"), + HTMLURL: github.Ptr("https://github.com/orgs/github/packages/npm/package/test-package"), + Visibility: github.Ptr("private"), + }, + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedPackages []*github.Package + expectedErrMsg string + }{ + { + name: "successful packages listing", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages", Method: "GET"}, + mockPackages, + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + }, + expectError: false, + expectedPackages: mockPackages, + }, + { + name: "successful packages listing with filters", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages", Method: "GET"}, + mockPackages, + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + "package_type": "container", + "visibility": "public", + }, + expectError: false, + expectedPackages: mockPackages, + }, + { + name: "organization not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages", Method: "GET"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Not Found"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "org": "nonexistent", + }, + expectError: true, + expectedErrMsg: "failed to list packages", + }, + { + name: "missing required parameter org", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]interface{}{}, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := ListOrgPackages(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.NoError(t, err) + require.True(t, result.IsError) + if tc.expectedErrMsg != "" { + errorContent := getErrorResult(t, result) + assert.Contains(t, errorContent.Text, tc.expectedErrMsg) + } + return + } + + require.NoError(t, err) + require.False(t, result.IsError) + + // Parse the result + textContent := getTextResult(t, result) + var returnedPackages []*github.Package + err = json.Unmarshal([]byte(textContent.Text), &returnedPackages) + require.NoError(t, err) + + assert.Equal(t, len(tc.expectedPackages), len(returnedPackages)) + for i, pkg := range returnedPackages { + assert.Equal(t, tc.expectedPackages[i].GetID(), pkg.GetID()) + assert.Equal(t, tc.expectedPackages[i].GetName(), pkg.GetName()) + } + }) + } +} + +func Test_GetOrgPackage(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := GetOrgPackage(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "get_org_package", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "org") + assert.Contains(t, tool.InputSchema.Properties, "package_type") + assert.Contains(t, tool.InputSchema.Properties, "package_name") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"org", "package_type", "package_name"}) + + // Setup mock package for success case + mockPackage := &github.Package{ + ID: github.Ptr(int64(1)), + Name: github.Ptr("github-mcp-server"), + PackageType: github.Ptr("container"), + HTMLURL: github.Ptr("https://github.com/orgs/github/packages/container/package/github-mcp-server"), + Visibility: github.Ptr("public"), + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedPackage *github.Package + expectedErrMsg string + }{ + { + name: "successful package retrieval", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}", Method: "GET"}, + mockPackage, + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + "package_type": "container", + "package_name": "github-mcp-server", + }, + expectError: false, + expectedPackage: mockPackage, + }, + { + name: "package not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}", Method: "GET"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Package not found"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + "package_type": "container", + "package_name": "nonexistent", + }, + expectError: true, + expectedErrMsg: "failed to get package", + }, + { + name: "missing required parameters", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]interface{}{ + "org": "github", + }, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := GetOrgPackage(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.NoError(t, err) + require.True(t, result.IsError) + if tc.expectedErrMsg != "" { + errorContent := getErrorResult(t, result) + assert.Contains(t, errorContent.Text, tc.expectedErrMsg) + } + return + } + + require.NoError(t, err) + require.False(t, result.IsError) + + // Parse the result + textContent := getTextResult(t, result) + var returnedPackage github.Package + err = json.Unmarshal([]byte(textContent.Text), &returnedPackage) + require.NoError(t, err) + + assert.Equal(t, tc.expectedPackage.GetID(), returnedPackage.GetID()) + assert.Equal(t, tc.expectedPackage.GetName(), returnedPackage.GetName()) + assert.Equal(t, tc.expectedPackage.GetPackageType(), returnedPackage.GetPackageType()) + }) + } +} + +func Test_ListPackageVersions(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := ListPackageVersions(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "list_package_versions", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "org") + assert.Contains(t, tool.InputSchema.Properties, "package_type") + assert.Contains(t, tool.InputSchema.Properties, "package_name") + assert.Contains(t, tool.InputSchema.Properties, "state") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"org", "package_type", "package_name"}) + + // Setup mock package versions for success case + mockVersions := []*github.PackageVersion{ + { + ID: github.Ptr(int64(123)), + Name: github.Ptr("v1.0.0"), + }, + { + ID: github.Ptr(int64(124)), + Name: github.Ptr("v1.0.1"), + }, + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedVersions []*github.PackageVersion + expectedErrMsg string + }{ + { + name: "successful versions listing", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}/versions", Method: "GET"}, + mockVersions, + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + "package_type": "container", + "package_name": "github-mcp-server", + }, + expectError: false, + expectedVersions: mockVersions, + }, + { + name: "successful versions listing with state filter", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}/versions", Method: "GET"}, + mockVersions, + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + "package_type": "container", + "package_name": "github-mcp-server", + "state": "active", + }, + expectError: false, + expectedVersions: mockVersions, + }, + { + name: "package not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}/versions", Method: "GET"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Not Found"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + "package_type": "container", + "package_name": "nonexistent", + }, + expectError: true, + expectedErrMsg: "failed to list package versions", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := ListPackageVersions(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.NoError(t, err) + require.True(t, result.IsError) + if tc.expectedErrMsg != "" { + errorContent := getErrorResult(t, result) + assert.Contains(t, errorContent.Text, tc.expectedErrMsg) + } + return + } + + require.NoError(t, err) + require.False(t, result.IsError) + + // Parse the result + textContent := getTextResult(t, result) + var returnedVersions []*github.PackageVersion + err = json.Unmarshal([]byte(textContent.Text), &returnedVersions) + require.NoError(t, err) + + assert.Equal(t, len(tc.expectedVersions), len(returnedVersions)) + }) + } +} + +func Test_GetPackageVersion(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := GetPackageVersion(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "get_package_version", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "org") + assert.Contains(t, tool.InputSchema.Properties, "package_type") + assert.Contains(t, tool.InputSchema.Properties, "package_name") + assert.Contains(t, tool.InputSchema.Properties, "package_version_id") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"org", "package_type", "package_name", "package_version_id"}) + + // Setup mock package version for success case + mockVersion := &github.PackageVersion{ + ID: github.Ptr(int64(123)), + Name: github.Ptr("v1.0.0"), + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedVersion *github.PackageVersion + expectedErrMsg string + }{ + { + name: "successful version retrieval", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}/versions/{package_version_id}", Method: "GET"}, + mockVersion, + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + "package_type": "container", + "package_name": "github-mcp-server", + "package_version_id": float64(123), + }, + expectError: false, + expectedVersion: mockVersion, + }, + { + name: "version not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}/versions/{package_version_id}", Method: "GET"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Not Found"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + "package_type": "container", + "package_name": "github-mcp-server", + "package_version_id": float64(999), + }, + expectError: true, + expectedErrMsg: "failed to get package version", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := GetPackageVersion(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.NoError(t, err) + require.True(t, result.IsError) + if tc.expectedErrMsg != "" { + errorContent := getErrorResult(t, result) + assert.Contains(t, errorContent.Text, tc.expectedErrMsg) + } + return + } + + require.NoError(t, err) + require.False(t, result.IsError) + + // Parse the result + textContent := getTextResult(t, result) + var returnedVersion github.PackageVersion + err = json.Unmarshal([]byte(textContent.Text), &returnedVersion) + require.NoError(t, err) + + assert.Equal(t, tc.expectedVersion.GetID(), returnedVersion.GetID()) + assert.Equal(t, tc.expectedVersion.GetName(), returnedVersion.GetName()) + }) + } +} + +func Test_ListUserPackages(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := ListUserPackages(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "list_user_packages", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "username") + assert.Contains(t, tool.InputSchema.Properties, "package_type") + assert.Contains(t, tool.InputSchema.Properties, "visibility") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"username"}) + + // Setup mock packages for success case + mockPackages := []*github.Package{ + { + ID: github.Ptr(int64(1)), + Name: github.Ptr("my-package"), + PackageType: github.Ptr("npm"), + Visibility: github.Ptr("public"), + }, + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedPackages []*github.Package + expectedErrMsg string + }{ + { + name: "successful user packages listing", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{Pattern: "/users/{username}/packages", Method: "GET"}, + mockPackages, + ), + ), + requestArgs: map[string]interface{}{ + "username": "octocat", + }, + expectError: false, + expectedPackages: mockPackages, + }, + { + name: "user not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/users/{username}/packages", Method: "GET"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Not Found"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "username": "nonexistent", + }, + expectError: true, + expectedErrMsg: "failed to list packages", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := ListUserPackages(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.NoError(t, err) + require.True(t, result.IsError) + if tc.expectedErrMsg != "" { + errorContent := getErrorResult(t, result) + assert.Contains(t, errorContent.Text, tc.expectedErrMsg) + } + return + } + + require.NoError(t, err) + require.False(t, result.IsError) + + // Parse the result + textContent := getTextResult(t, result) + var returnedPackages []*github.Package + err = json.Unmarshal([]byte(textContent.Text), &returnedPackages) + require.NoError(t, err) + + assert.Equal(t, len(tc.expectedPackages), len(returnedPackages)) + }) + } +} + +func Test_DeleteOrgPackage(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := DeleteOrgPackage(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "delete_org_package", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "org") + assert.Contains(t, tool.InputSchema.Properties, "package_type") + assert.Contains(t, tool.InputSchema.Properties, "package_name") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"org", "package_type", "package_name"}) + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedErrMsg string + }{ + { + name: "successful package deletion", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}", Method: "DELETE"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + }), + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + "package_type": "container", + "package_name": "test-package", + }, + expectError: false, + }, + { + name: "package not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}", Method: "DELETE"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Package not found"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + "package_type": "container", + "package_name": "nonexistent", + }, + expectError: true, + expectedErrMsg: "failed to delete package", + }, + { + name: "insufficient permissions", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}", Method: "DELETE"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"message": "Forbidden - requires delete:packages scope"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + "package_type": "container", + "package_name": "test-package", + }, + expectError: true, + expectedErrMsg: "failed to delete package", + }, + { + name: "missing required parameters", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]interface{}{ + "org": "github", + }, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := DeleteOrgPackage(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.NoError(t, err) + require.True(t, result.IsError) + if tc.expectedErrMsg != "" { + errorContent := getErrorResult(t, result) + assert.Contains(t, errorContent.Text, tc.expectedErrMsg) + } + return + } + + verifyDeletionSuccess(t, result, err) + }) + } +} + +func Test_DeleteOrgPackageVersion(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := DeleteOrgPackageVersion(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "delete_org_package_version", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "org") + assert.Contains(t, tool.InputSchema.Properties, "package_type") + assert.Contains(t, tool.InputSchema.Properties, "package_name") + assert.Contains(t, tool.InputSchema.Properties, "package_version_id") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"org", "package_type", "package_name", "package_version_id"}) + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedErrMsg string + }{ + { + name: "successful version deletion", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}/versions/{package_version_id}", Method: "DELETE"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + }), + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + "package_type": "container", + "package_name": "test-package", + "package_version_id": float64(123), + }, + expectError: false, + }, + { + name: "version not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}/versions/{package_version_id}", Method: "DELETE"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Version not found"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + "package_type": "container", + "package_name": "test-package", + "package_version_id": float64(999), + }, + expectError: true, + expectedErrMsg: "failed to delete package version", + }, + { + name: "insufficient permissions", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}/versions/{package_version_id}", Method: "DELETE"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"message": "Forbidden - requires delete:packages scope"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "org": "github", + "package_type": "container", + "package_name": "test-package", + "package_version_id": float64(123), + }, + expectError: true, + expectedErrMsg: "failed to delete package version", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := DeleteOrgPackageVersion(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.NoError(t, err) + require.True(t, result.IsError) + if tc.expectedErrMsg != "" { + errorContent := getErrorResult(t, result) + assert.Contains(t, errorContent.Text, tc.expectedErrMsg) + } + return + } + + verifyDeletionSuccess(t, result, err) + }) + } +} + +func Test_DeleteUserPackage(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := DeleteUserPackage(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "delete_user_package", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "package_type") + assert.Contains(t, tool.InputSchema.Properties, "package_name") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"package_type", "package_name"}) + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedErrMsg string + }{ + { + name: "successful user package deletion", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/user/packages/{package_type}/{package_name}", Method: "DELETE"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + }), + ), + ), + requestArgs: map[string]interface{}{ + "package_type": "npm", + "package_name": "my-package", + }, + expectError: false, + }, + { + name: "package not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/user/packages/{package_type}/{package_name}", Method: "DELETE"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Package not found"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "package_type": "npm", + "package_name": "nonexistent", + }, + expectError: true, + expectedErrMsg: "failed to delete package", + }, + { + name: "missing required parameters", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]interface{}{ + "package_type": "npm", + }, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := DeleteUserPackage(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.NoError(t, err) + require.True(t, result.IsError) + if tc.expectedErrMsg != "" { + errorContent := getErrorResult(t, result) + assert.Contains(t, errorContent.Text, tc.expectedErrMsg) + } + return + } + + verifyDeletionSuccess(t, result, err) + }) + } +} + +func Test_DeleteUserPackageVersion(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := DeleteUserPackageVersion(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "delete_user_package_version", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "package_type") + assert.Contains(t, tool.InputSchema.Properties, "package_name") + assert.Contains(t, tool.InputSchema.Properties, "package_version_id") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"package_type", "package_name", "package_version_id"}) + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedErrMsg string + }{ + { + name: "successful user version deletion", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/user/packages/{package_type}/{package_name}/versions/{package_version_id}", Method: "DELETE"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + }), + ), + ), + requestArgs: map[string]interface{}{ + "package_type": "npm", + "package_name": "my-package", + "package_version_id": float64(123), + }, + expectError: false, + }, + { + name: "version not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{Pattern: "/user/packages/{package_type}/{package_name}/versions/{package_version_id}", Method: "DELETE"}, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Version not found"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "package_type": "npm", + "package_name": "my-package", + "package_version_id": float64(999), + }, + expectError: true, + expectedErrMsg: "failed to delete version", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := DeleteUserPackageVersion(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.NoError(t, err) + require.True(t, result.IsError) + if tc.expectedErrMsg != "" { + errorContent := getErrorResult(t, result) + assert.Contains(t, errorContent.Text, tc.expectedErrMsg) + } + return + } + + verifyDeletionSuccess(t, result, err) + }) + } +} diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 659286e02..a321bb827 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -107,6 +107,10 @@ var ( ID: "labels", Description: "GitHub Labels related tools", } + ToolsetMetadataPackages = ToolsetMetadata{ + ID: "packages", + Description: "GitHub Packages related tools for managing and viewing package metadata, versions, and deletion operations", + } ) func AvailableTools() []ToolsetMetadata { @@ -130,6 +134,7 @@ func AvailableTools() []ToolsetMetadata { ToolsetMetadataStargazers, ToolsetMetadataDynamic, ToolsetLabels, + ToolsetMetadataPackages, } } @@ -349,6 +354,20 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG // create or update toolsets.NewServerTool(LabelWrite(getGQLClient, t)), ) + packages := toolsets.NewToolset(ToolsetMetadataPackages.ID, ToolsetMetadataPackages.Description). + AddReadTools( + toolsets.NewServerTool(ListOrgPackages(getClient, t)), + toolsets.NewServerTool(GetOrgPackage(getClient, t)), + toolsets.NewServerTool(ListPackageVersions(getClient, t)), + toolsets.NewServerTool(GetPackageVersion(getClient, t)), + toolsets.NewServerTool(ListUserPackages(getClient, t)), + ). + AddWriteTools( + toolsets.NewServerTool(DeleteOrgPackage(getClient, t)), + toolsets.NewServerTool(DeleteOrgPackageVersion(getClient, t)), + toolsets.NewServerTool(DeleteUserPackage(getClient, t)), + toolsets.NewServerTool(DeleteUserPackageVersion(getClient, t)), + ) // Add toolsets to the group tsg.AddToolset(contextTools) tsg.AddToolset(repos) @@ -368,6 +387,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG tsg.AddToolset(projects) tsg.AddToolset(stargazers) tsg.AddToolset(labels) + tsg.AddToolset(packages) return tsg }