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