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
}