diff --git a/README.md b/README.md index c7243033b..7ce8e35b9 100644 --- a/README.md +++ b/README.md @@ -466,6 +466,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 | @@ -871,6 +872,47 @@ Options are:
+Packages + +- **packages_read** - Read package information + - `method`: Action to specify what package data needs to be retrieved from GitHub. +Possible options: + 1. list_org_packages - List packages for a GitHub organization. Requires 'org' parameter. Supports optional 'package_type' and 'visibility' filters. + 2. get_org_package - Get details of a specific package for an organization. Requires 'org', 'package_type', and 'package_name' parameters. + 3. list_package_versions - List versions of a package for an organization. Requires 'org', 'package_type', and 'package_name' parameters. Supports optional 'state' filter. + 4. get_package_version - Get details of a specific package version. Requires 'org', 'package_type', 'package_name', and 'package_version_id' parameters. + 5. list_user_packages - List packages for a GitHub user. Requires 'username' parameter. Supports optional 'package_type' and 'visibility' filters. + +Note: Download statistics are not available via the GitHub REST API. (string, required) + - `org`: Organization name (required for org-related methods) (string, optional) + - `package_name`: Package name (required for get_org_package, list_package_versions, and get_package_version methods) (string, optional) + - `package_type`: Package type (string, optional) + - `package_version_id`: Package version ID (required for get_package_version method) (number, optional) + - `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 (optional for list_package_versions method) (string, optional) + - `username`: GitHub username (required for list_user_packages method) (string, optional) + - `visibility`: Filter by package visibility (optional for list methods) (string, optional) + +- **packages_write** - Delete operations on packages + - `method`: The write operation to perform on packages. + +Available methods: + 1. delete_org_package - Delete an entire package from an organization. This will delete all versions of the package. Requires 'org', 'package_type', and 'package_name' parameters. + 2. delete_org_package_version - Delete a specific version of a package from an organization. Requires 'org', 'package_type', 'package_name', and 'package_version_id' parameters. + 3. delete_user_package - Delete an entire package from the authenticated user's account. This will delete all versions of the package. Requires 'package_type' and 'package_name' parameters. + 4. delete_user_package_version - Delete a specific version of a package from the authenticated user's account. Requires 'package_type', 'package_name', and 'package_version_id' parameters. + +All operations require delete:packages scope. (string, required) + - `org`: Organization name (required for delete_org_package and delete_org_package_version methods) (string, optional) + - `package_name`: Package name (required for all methods) (string, required) + - `package_type`: Package type (required for all methods) (string, required) + - `package_version_id`: Package version ID (required for delete_org_package_version and delete_user_package_version methods) (number, optional) + +
+ +
+ Projects - **add_project_item** - Add project item diff --git a/docs/remote-server.md b/docs/remote-server.md index 1030911ef..bd4405f4f 100644 --- a/docs/remote-server.md +++ b/docs/remote-server.md @@ -19,7 +19,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to | Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) | |----------------|--------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Default | ["Default" toolset](../README.md#default-toolset) | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Freadonly%22%7D) | +| all | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Freadonly%22%7D) | | Actions | GitHub Actions workflows and CI/CD operations | https://api.githubcopilot.com/mcp/x/actions | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-actions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Factions%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/actions/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-actions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Factions%2Freadonly%22%7D) | | Code Security | Code security related tools, such as GitHub Code Scanning | https://api.githubcopilot.com/mcp/x/code_security | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_security&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_security%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/code_security/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_security&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_security%2Freadonly%22%7D) | | Dependabot | Dependabot tools | https://api.githubcopilot.com/mcp/x/dependabot | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/dependabot/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%2Freadonly%22%7D) | @@ -31,6 +31,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__/packages_read.snap b/pkg/github/__toolsnaps__/packages_read.snap new file mode 100644 index 000000000..2f817c277 --- /dev/null +++ b/pkg/github/__toolsnaps__/packages_read.snap @@ -0,0 +1,83 @@ +{ + "annotations": { + "readOnlyHint": true, + "title": "Read package information" + }, + "description": "Get information about GitHub packages for organizations and users. Supports listing packages, getting package details, and inspecting package versions.", + "inputSchema": { + "type": "object", + "required": [ + "method" + ], + "properties": { + "method": { + "type": "string", + "description": "Action to specify what package data needs to be retrieved from GitHub.\nPossible options:\n 1. list_org_packages - List packages for a GitHub organization. Requires 'org' parameter. Supports optional 'package_type' and 'visibility' filters.\n 2. get_org_package - Get details of a specific package for an organization. Requires 'org', 'package_type', and 'package_name' parameters.\n 3. list_package_versions - List versions of a package for an organization. Requires 'org', 'package_type', and 'package_name' parameters. Supports optional 'state' filter.\n 4. get_package_version - Get details of a specific package version. Requires 'org', 'package_type', 'package_name', and 'package_version_id' parameters.\n 5. list_user_packages - List packages for a GitHub user. Requires 'username' parameter. Supports optional 'package_type' and 'visibility' filters.\n\nNote: Download statistics are not available via the GitHub REST API.", + "enum": [ + "list_org_packages", + "get_org_package", + "list_package_versions", + "get_package_version", + "list_user_packages" + ] + }, + "org": { + "type": "string", + "description": "Organization name (required for org-related methods)" + }, + "package_name": { + "type": "string", + "description": "Package name (required for get_org_package, list_package_versions, and get_package_version methods)" + }, + "package_type": { + "type": "string", + "description": "Package type", + "enum": [ + "npm", + "maven", + "rubygems", + "docker", + "nuget", + "container" + ] + }, + "package_version_id": { + "type": "number", + "description": "Package version ID (required for get_package_version method)" + }, + "page": { + "type": "number", + "description": "Page number for pagination (min 1)", + "minimum": 1 + }, + "perPage": { + "type": "number", + "description": "Results per page for pagination (min 1, max 100)", + "minimum": 1, + "maximum": 100 + }, + "state": { + "type": "string", + "description": "Filter by version state (optional for list_package_versions method)", + "enum": [ + "active", + "deleted" + ] + }, + "username": { + "type": "string", + "description": "GitHub username (required for list_user_packages method)" + }, + "visibility": { + "type": "string", + "description": "Filter by package visibility (optional for list methods)", + "enum": [ + "public", + "private", + "internal" + ] + } + } + }, + "name": "packages_read" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/packages_write.snap b/pkg/github/__toolsnaps__/packages_write.snap new file mode 100644 index 000000000..07b011cf5 --- /dev/null +++ b/pkg/github/__toolsnaps__/packages_write.snap @@ -0,0 +1,51 @@ +{ + "annotations": { + "title": "Delete operations on packages" + }, + "description": "Delete packages and package versions for organizations and users. All operations require delete:packages scope.", + "inputSchema": { + "type": "object", + "required": [ + "method", + "package_type", + "package_name" + ], + "properties": { + "method": { + "type": "string", + "description": "The write operation to perform on packages.\n\nAvailable methods:\n 1. delete_org_package - Delete an entire package from an organization. This will delete all versions of the package. Requires 'org', 'package_type', and 'package_name' parameters.\n 2. delete_org_package_version - Delete a specific version of a package from an organization. Requires 'org', 'package_type', 'package_name', and 'package_version_id' parameters.\n 3. delete_user_package - Delete an entire package from the authenticated user's account. This will delete all versions of the package. Requires 'package_type' and 'package_name' parameters.\n 4. delete_user_package_version - Delete a specific version of a package from the authenticated user's account. Requires 'package_type', 'package_name', and 'package_version_id' parameters.\n\nAll operations require delete:packages scope.", + "enum": [ + "delete_org_package", + "delete_org_package_version", + "delete_user_package", + "delete_user_package_version" + ] + }, + "org": { + "type": "string", + "description": "Organization name (required for delete_org_package and delete_org_package_version methods)" + }, + "package_name": { + "type": "string", + "description": "Package name (required for all methods)" + }, + "package_type": { + "type": "string", + "description": "Package type (required for all methods)", + "enum": [ + "npm", + "maven", + "rubygems", + "docker", + "nuget", + "container" + ] + }, + "package_version_id": { + "type": "number", + "description": "Package version ID (required for delete_org_package_version and delete_user_package_version methods)" + } + } + }, + "name": "packages_write" +} \ No newline at end of file diff --git a/pkg/github/packages.go b/pkg/github/packages.go new file mode 100644 index 000000000..b27491fde --- /dev/null +++ b/pkg/github/packages.go @@ -0,0 +1,549 @@ +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/github/github-mcp-server/pkg/utils" + "github.com/google/go-github/v79/github" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// 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, any, error) { + 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("deletion failed: %s", string(body))), nil, nil + } + + result := map[string]interface{}{ + "success": true, + "message": successMessage, + } + + r, err := json.Marshal(result) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return utils.NewToolResultText(string(r)), nil, nil +} + +// PackagesRead creates a consolidated tool to read package information. +func PackagesRead(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "method": { + Type: "string", + Description: `Action to specify what package data needs to be retrieved from GitHub. +Possible options: + 1. list_org_packages - List packages for a GitHub organization. Requires 'org' parameter. Supports optional 'package_type' and 'visibility' filters. + 2. get_org_package - Get details of a specific package for an organization. Requires 'org', 'package_type', and 'package_name' parameters. + 3. list_package_versions - List versions of a package for an organization. Requires 'org', 'package_type', and 'package_name' parameters. Supports optional 'state' filter. + 4. get_package_version - Get details of a specific package version. Requires 'org', 'package_type', 'package_name', and 'package_version_id' parameters. + 5. list_user_packages - List packages for a GitHub user. Requires 'username' parameter. Supports optional 'package_type' and 'visibility' filters. + +Note: Download statistics are not available via the GitHub REST API.`, + Enum: []any{"list_org_packages", "get_org_package", "list_package_versions", "get_package_version", "list_user_packages"}, + }, + "org": { + Type: "string", + Description: "Organization name (required for org-related methods)", + }, + "username": { + Type: "string", + Description: "GitHub username (required for list_user_packages method)", + }, + "package_type": { + Type: "string", + Description: "Package type", + Enum: []any{"npm", "maven", "rubygems", "docker", "nuget", "container"}, + }, + "package_name": { + Type: "string", + Description: "Package name (required for get_org_package, list_package_versions, and get_package_version methods)", + }, + "package_version_id": { + Type: "number", + Description: "Package version ID (required for get_package_version method)", + }, + "visibility": { + Type: "string", + Description: "Filter by package visibility (optional for list methods)", + Enum: []any{"public", "private", "internal"}, + }, + "state": { + Type: "string", + Description: "Filter by version state (optional for list_package_versions method)", + Enum: []any{"active", "deleted"}, + }, + }, + Required: []string{"method"}, + } + WithPagination(schema) + + return mcp.Tool{ + Name: "packages_read", + Description: t("TOOL_PACKAGES_READ_DESCRIPTION", "Get information about GitHub packages for organizations and users. Supports listing packages, getting package details, and inspecting package versions."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_PACKAGES_READ_USER_TITLE", "Read package information"), + ReadOnlyHint: true, + }, + InputSchema: schema, + }, + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + method, err := RequiredParam[string](args, "method") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + client, err := getClient(ctx) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil + } + + pagination, err := OptionalPaginationParams(args) + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + switch method { + case "list_org_packages": + result, err := listOrgPackagesImpl(ctx, client, args, pagination) + return result, nil, err + case "get_org_package": + result, err := getOrgPackageImpl(ctx, client, args) + return result, nil, err + case "list_package_versions": + result, err := listPackageVersionsImpl(ctx, client, args, pagination) + return result, nil, err + case "get_package_version": + result, err := getPackageVersionImpl(ctx, client, args) + return result, nil, err + case "list_user_packages": + result, err := listUserPackagesImpl(ctx, client, args, pagination) + return result, nil, err + default: + return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil + } + } +} + +func listOrgPackagesImpl(ctx context.Context, client *github.Client, args map[string]any, pagination PaginationParams) (*mcp.CallToolResult, error) { + org, err := RequiredParam[string](args, "org") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageType, err := OptionalParam[string](args, "package_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + visibility, err := OptionalParam[string](args, "visibility") + if err != nil { + return utils.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) + } + + 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 utils.NewToolResultText(string(r)), nil +} + +func getOrgPackageImpl(ctx context.Context, client *github.Client, args map[string]any) (*mcp.CallToolResult, error) { + org, err := RequiredParam[string](args, "org") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageType, err := RequiredParam[string](args, "package_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageName, err := RequiredParam[string](args, "package_name") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + 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 utils.NewToolResultText(string(r)), nil +} + +func listPackageVersionsImpl(ctx context.Context, client *github.Client, args map[string]any, pagination PaginationParams) (*mcp.CallToolResult, error) { + org, err := RequiredParam[string](args, "org") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageType, err := RequiredParam[string](args, "package_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageName, err := RequiredParam[string](args, "package_name") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + state, err := OptionalParam[string](args, "state") + if err != nil { + return utils.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) + } + + 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 utils.NewToolResultText(string(r)), nil +} + +func getPackageVersionImpl(ctx context.Context, client *github.Client, args map[string]any) (*mcp.CallToolResult, error) { + org, err := RequiredParam[string](args, "org") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageType, err := RequiredParam[string](args, "package_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageName, err := RequiredParam[string](args, "package_name") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageVersionID, err := RequiredParam[float64](args, "package_version_id") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + 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 utils.NewToolResultText(string(r)), nil +} + +func listUserPackagesImpl(ctx context.Context, client *github.Client, args map[string]any, pagination PaginationParams) (*mcp.CallToolResult, error) { + username, err := RequiredParam[string](args, "username") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageType, err := OptionalParam[string](args, "package_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + visibility, err := OptionalParam[string](args, "visibility") + if err != nil { + return utils.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) + } + + 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 utils.NewToolResultText(string(r)), nil +} + +// PackagesWrite creates a consolidated tool for package deletion operations. +// Requires delete:packages scope in addition to read:packages. +func PackagesWrite(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "method": { + Type: "string", + Description: `The write operation to perform on packages. + +Available methods: + 1. delete_org_package - Delete an entire package from an organization. This will delete all versions of the package. Requires 'org', 'package_type', and 'package_name' parameters. + 2. delete_org_package_version - Delete a specific version of a package from an organization. Requires 'org', 'package_type', 'package_name', and 'package_version_id' parameters. + 3. delete_user_package - Delete an entire package from the authenticated user's account. This will delete all versions of the package. Requires 'package_type' and 'package_name' parameters. + 4. delete_user_package_version - Delete a specific version of a package from the authenticated user's account. Requires 'package_type', 'package_name', and 'package_version_id' parameters. + +All operations require delete:packages scope.`, + Enum: []any{"delete_org_package", "delete_org_package_version", "delete_user_package", "delete_user_package_version"}, + }, + "org": { + Type: "string", + Description: "Organization name (required for delete_org_package and delete_org_package_version methods)", + }, + "package_type": { + Type: "string", + Description: "Package type (required for all methods)", + Enum: []any{"npm", "maven", "rubygems", "docker", "nuget", "container"}, + }, + "package_name": { + Type: "string", + Description: "Package name (required for all methods)", + }, + "package_version_id": { + Type: "number", + Description: "Package version ID (required for delete_org_package_version and delete_user_package_version methods)", + }, + }, + Required: []string{"method", "package_type", "package_name"}, + } + + return mcp.Tool{ + Name: "packages_write", + Description: t("TOOL_PACKAGES_WRITE_DESCRIPTION", "Delete packages and package versions for organizations and users. All operations require delete:packages scope."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_PACKAGES_WRITE_USER_TITLE", "Delete operations on packages"), + ReadOnlyHint: false, + }, + InputSchema: schema, + }, + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + method, err := RequiredParam[string](args, "method") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + switch method { + case "delete_org_package": + result, err := deleteOrgPackageImpl(ctx, client, args) + return result, nil, err + case "delete_org_package_version": + result, err := deleteOrgPackageVersionImpl(ctx, client, args) + return result, nil, err + case "delete_user_package": + result, err := deleteUserPackageImpl(ctx, client, args) + return result, nil, err + case "delete_user_package_version": + result, err := deleteUserPackageVersionImpl(ctx, client, args) + return result, nil, err + default: + return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil + } + } +} + +func deleteOrgPackageImpl(ctx context.Context, client *github.Client, args map[string]any) (*mcp.CallToolResult, error) { + org, err := RequiredParam[string](args, "org") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageType, err := RequiredParam[string](args, "package_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageName, err := RequiredParam[string](args, "package_name") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + 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 + } + + result, _, err := handleDeletionResponse(resp, fmt.Sprintf("Package '%s' deleted successfully from organization '%s'", packageName, org)) + return result, err +} + +func deleteOrgPackageVersionImpl(ctx context.Context, client *github.Client, args map[string]any) (*mcp.CallToolResult, error) { + org, err := RequiredParam[string](args, "org") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageType, err := RequiredParam[string](args, "package_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageName, err := RequiredParam[string](args, "package_name") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageVersionID, err := RequiredParam[float64](args, "package_version_id") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + 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 + } + + result, _, err := handleDeletionResponse(resp, fmt.Sprintf("Package version %d deleted successfully from package '%s'", int64(packageVersionID), packageName)) + return result, err +} + +func deleteUserPackageImpl(ctx context.Context, client *github.Client, args map[string]any) (*mcp.CallToolResult, error) { + packageType, err := RequiredParam[string](args, "package_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageName, err := RequiredParam[string](args, "package_name") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + 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 + } + + result, _, err := handleDeletionResponse(resp, fmt.Sprintf("Package '%s' deleted successfully", packageName)) + return result, err +} + +func deleteUserPackageVersionImpl(ctx context.Context, client *github.Client, args map[string]any) (*mcp.CallToolResult, error) { + packageType, err := RequiredParam[string](args, "package_type") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageName, err := RequiredParam[string](args, "package_name") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + packageVersionID, err := RequiredParam[float64](args, "package_version_id") + if err != nil { + return utils.NewToolResultError(err.Error()), nil + } + + 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 + } + + result, _, err := handleDeletionResponse(resp, fmt.Sprintf("Package version %d deleted successfully from package '%s'", int64(packageVersionID), packageName)) + return result, err +} diff --git a/pkg/github/packages_test.go b/pkg/github/packages_test.go new file mode 100644 index 000000000..1c9b80b41 --- /dev/null +++ b/pkg/github/packages_test.go @@ -0,0 +1,383 @@ +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/v79/github" + "github.com/google/jsonschema-go/jsonschema" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// verifyDeletionSuccess is a helper function to verify deletion operation success. +func verifyDeletionSuccess(t *testing.T, result *mcp.CallToolResult, err error) { + t.Helper() + + require.NoError(t, err) + require.False(t, result.IsError) + + 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_PackagesRead(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := PackagesRead(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "packages_read", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.True(t, tool.Annotations.ReadOnlyHint, "PackagesRead tool should be read-only") + schema := tool.InputSchema.(*jsonschema.Schema) + assert.Contains(t, schema.Properties, "method") + assert.ElementsMatch(t, schema.Required, []string{"method"}) +} + +func Test_PackagesRead_ListOrgPackages(t *testing.T) { + mockPackages := []*github.Package{ + { + ID: github.Ptr(int64(1)), + Name: github.Ptr("github-mcp-server"), + PackageType: github.Ptr("container"), + Visibility: github.Ptr("public"), + }, + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedPackages []*github.Package + expectedErrMsg string + }{ + { + name: "successful list", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages", Method: "GET"}, + mockPackages, + ), + ), + requestArgs: map[string]interface{}{ + "method": "list_org_packages", + "org": "github", + }, + expectError: false, + expectedPackages: mockPackages, + }, + { + name: "successful list with package_type filter", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages", Method: "GET"}, + mockPackages, + ), + ), + requestArgs: map[string]interface{}{ + "method": "list_org_packages", + "org": "github", + "package_type": "container", + }, + expectError: false, + expectedPackages: mockPackages, + }, + { + name: "successful list with visibility filter", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages", Method: "GET"}, + mockPackages, + ), + ), + requestArgs: map[string]interface{}{ + "method": "list_org_packages", + "org": "github", + "visibility": "public", + }, + expectError: false, + expectedPackages: mockPackages, + }, + { + name: "missing org parameter", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]interface{}{ + "method": "list_org_packages", + }, + expectError: true, + }, + { + 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{}{ + "method": "list_org_packages", + "org": "nonexistent-org", + }, + expectError: true, + expectedErrMsg: "failed to list packages", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + client := github.NewClient(tc.mockedClient) + _, handler := PackagesRead(stubGetClientFn(client), translations.NullTranslationHelper) + result, _, err := handler(context.Background(), nil, tc.requestArgs) + + 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 and verify the result + textContent := getTextResult(t, result) + var returnedPackages []*github.Package + err = json.Unmarshal([]byte(textContent.Text), &returnedPackages) + require.NoError(t, err) + + assert.Len(t, returnedPackages, len(tc.expectedPackages)) + for i, pkg := range returnedPackages { + assert.Equal(t, *tc.expectedPackages[i].ID, *pkg.ID) + assert.Equal(t, *tc.expectedPackages[i].Name, *pkg.Name) + assert.Equal(t, *tc.expectedPackages[i].PackageType, *pkg.PackageType) + assert.Equal(t, *tc.expectedPackages[i].Visibility, *pkg.Visibility) + } + }) + } +} + +func Test_PackagesRead_GetOrgPackage(t *testing.T) { + mockPackage := &github.Package{ + ID: github.Ptr(int64(1)), + Name: github.Ptr("test-package"), + } + + client := mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}", Method: "GET"}, + mockPackage, + ), + ) + + ghClient := github.NewClient(client) + _, handler := PackagesRead(stubGetClientFn(ghClient), translations.NullTranslationHelper) + + result, _, err := handler(context.Background(), nil, map[string]interface{}{ + "method": "get_org_package", + "org": "github", + "package_type": "container", + "package_name": "test-package", + }) + + require.NoError(t, err) + require.False(t, result.IsError) +} + +func Test_PackagesRead_ListPackageVersions(t *testing.T) { + mockVersions := []*github.PackageVersion{ + {ID: github.Ptr(int64(123)), Name: github.Ptr("v1.0.0")}, + } + + client := mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}/versions", Method: "GET"}, + mockVersions, + ), + ) + + ghClient := github.NewClient(client) + _, handler := PackagesRead(stubGetClientFn(ghClient), translations.NullTranslationHelper) + + result, _, err := handler(context.Background(), nil, map[string]interface{}{ + "method": "list_package_versions", + "org": "github", + "package_type": "container", + "package_name": "test-package", + }) + + require.NoError(t, err) + require.False(t, result.IsError) +} + +func Test_PackagesRead_GetPackageVersion(t *testing.T) { + mockVersion := &github.PackageVersion{ + ID: github.Ptr(int64(123)), + Name: github.Ptr("v1.0.0"), + } + + client := mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{Pattern: "/orgs/{org}/packages/{package_type}/{package_name}/versions/{package_version_id}", Method: "GET"}, + mockVersion, + ), + ) + + ghClient := github.NewClient(client) + _, handler := PackagesRead(stubGetClientFn(ghClient), translations.NullTranslationHelper) + + result, _, err := handler(context.Background(), nil, map[string]interface{}{ + "method": "get_package_version", + "org": "github", + "package_type": "container", + "package_name": "test-package", + "package_version_id": float64(123), + }) + + require.NoError(t, err) + require.False(t, result.IsError) +} + +func Test_PackagesRead_ListUserPackages(t *testing.T) { + mockPackages := []*github.Package{ + {ID: github.Ptr(int64(1)), Name: github.Ptr("user-package")}, + } + + client := mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.EndpointPattern{Pattern: "/users/{username}/packages", Method: "GET"}, + mockPackages, + ), + ) + + ghClient := github.NewClient(client) + _, handler := PackagesRead(stubGetClientFn(ghClient), translations.NullTranslationHelper) + + result, _, err := handler(context.Background(), nil, map[string]interface{}{ + "method": "list_user_packages", + "username": "testuser", + }) + + require.NoError(t, err) + require.False(t, result.IsError) +} + +func Test_PackagesWrite(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := PackagesWrite(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "packages_write", tool.Name) + schema := tool.InputSchema.(*jsonschema.Schema) + assert.ElementsMatch(t, schema.Required, []string{"method", "package_type", "package_name"}) + assert.False(t, tool.Annotations.ReadOnlyHint, "PackagesWrite tool should not be read-only") +} + +func Test_PackagesWrite_DeleteOrgPackage(t *testing.T) { + client := 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) + }), + ), + ) + + ghClient := github.NewClient(client) + _, handler := PackagesWrite(stubGetClientFn(ghClient), translations.NullTranslationHelper) + + result, _, err := handler(context.Background(), nil, map[string]interface{}{ + "method": "delete_org_package", + "org": "github", + "package_type": "container", + "package_name": "test-package", + }) + + verifyDeletionSuccess(t, result, err) +} + +func Test_PackagesWrite_DeleteOrgPackageVersion(t *testing.T) { + client := 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) + }), + ), + ) + + ghClient := github.NewClient(client) + _, handler := PackagesWrite(stubGetClientFn(ghClient), translations.NullTranslationHelper) + + result, _, err := handler(context.Background(), nil, map[string]interface{}{ + "method": "delete_org_package_version", + "org": "github", + "package_type": "container", + "package_name": "test-package", + "package_version_id": float64(123), + }) + + verifyDeletionSuccess(t, result, err) +} + +func Test_PackagesWrite_DeleteUserPackage(t *testing.T) { + client := 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) + }), + ), + ) + + ghClient := github.NewClient(client) + _, handler := PackagesWrite(stubGetClientFn(ghClient), translations.NullTranslationHelper) + + result, _, err := handler(context.Background(), nil, map[string]interface{}{ + "method": "delete_user_package", + "package_type": "container", + "package_name": "test-package", + }) + + verifyDeletionSuccess(t, result, err) +} + +func Test_PackagesWrite_DeleteUserPackageVersion(t *testing.T) { + client := 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) + }), + ), + ) + + ghClient := github.NewClient(client) + _, handler := PackagesWrite(stubGetClientFn(ghClient), translations.NullTranslationHelper) + + result, _, err := handler(context.Background(), nil, map[string]interface{}{ + "method": "delete_user_package_version", + "package_type": "container", + "package_name": "test-package", + "package_version_id": float64(123), + }) + + verifyDeletionSuccess(t, result, err) +} diff --git a/pkg/github/tools.go b/pkg/github/tools.go index d37af98b8..77ef8dd22 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -112,6 +112,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 { @@ -135,6 +139,7 @@ func AvailableTools() []ToolsetMetadata { ToolsetMetadataStargazers, ToolsetMetadataDynamic, ToolsetLabels, + ToolsetMetadataPackages, } } @@ -356,7 +361,13 @@ 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(PackagesRead(getClient, t)), + ). + AddWriteTools( + toolsets.NewServerTool(PackagesWrite(getClient, t)), + ) // Add toolsets to the group tsg.AddToolset(contextTools) tsg.AddToolset(repos) @@ -377,6 +388,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG tsg.AddToolset(projects) tsg.AddToolset(stargazers) tsg.AddToolset(labels) + tsg.AddToolset(packages) return tsg }