diff --git a/pkg/github/__toolsnaps__/add_project_item.snap b/pkg/github/__toolsnaps__/add_project_item.snap deleted file mode 100644 index e6a5cc3c4..000000000 --- a/pkg/github/__toolsnaps__/add_project_item.snap +++ /dev/null @@ -1,47 +0,0 @@ -{ - "annotations": { - "title": "Add project item" - }, - "description": "Add a specific Project item for a user or org", - "inputSchema": { - "properties": { - "item_id": { - "description": "The numeric ID of the issue or pull request to add to the project.", - "type": "number" - }, - "item_type": { - "description": "The item's type, either issue or pull_request.", - "enum": [ - "issue", - "pull_request" - ], - "type": "string" - }, - "owner": { - "description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - "type": "string" - }, - "owner_type": { - "description": "Owner type", - "enum": [ - "user", - "org" - ], - "type": "string" - }, - "project_number": { - "description": "The project's number.", - "type": "number" - } - }, - "required": [ - "owner_type", - "owner", - "project_number", - "item_type", - "item_id" - ], - "type": "object" - }, - "name": "add_project_item" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/cancel_workflow_run.snap b/pkg/github/__toolsnaps__/cancel_workflow_run.snap deleted file mode 100644 index 40bcae740..000000000 --- a/pkg/github/__toolsnaps__/cancel_workflow_run.snap +++ /dev/null @@ -1,29 +0,0 @@ -{ - "annotations": { - "title": "Cancel workflow run" - }, - "description": "Cancel a workflow run", - "inputSchema": { - "properties": { - "owner": { - "description": "Repository owner", - "type": "string" - }, - "repo": { - "description": "Repository name", - "type": "string" - }, - "run_id": { - "description": "The unique identifier of the workflow run", - "type": "number" - } - }, - "required": [ - "owner", - "repo", - "run_id" - ], - "type": "object" - }, - "name": "cancel_workflow_run" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/delete_project_item.snap b/pkg/github/__toolsnaps__/delete_project_item.snap deleted file mode 100644 index 819fb8474..000000000 --- a/pkg/github/__toolsnaps__/delete_project_item.snap +++ /dev/null @@ -1,39 +0,0 @@ -{ - "annotations": { - "destructiveHint": true, - "title": "Delete project item" - }, - "description": "Delete a specific Project item for a user or org", - "inputSchema": { - "properties": { - "item_id": { - "description": "The internal project item ID to delete from the project (not the issue or pull request ID).", - "type": "number" - }, - "owner": { - "description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - "type": "string" - }, - "owner_type": { - "description": "Owner type", - "enum": [ - "user", - "org" - ], - "type": "string" - }, - "project_number": { - "description": "The project's number.", - "type": "number" - } - }, - "required": [ - "owner_type", - "owner", - "project_number", - "item_id" - ], - "type": "object" - }, - "name": "delete_project_item" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/delete_workflow_run_logs.snap b/pkg/github/__toolsnaps__/delete_workflow_run_logs.snap deleted file mode 100644 index 2e2de7331..000000000 --- a/pkg/github/__toolsnaps__/delete_workflow_run_logs.snap +++ /dev/null @@ -1,30 +0,0 @@ -{ - "annotations": { - "destructiveHint": true, - "title": "Delete workflow logs" - }, - "description": "Delete logs for a workflow run", - "inputSchema": { - "properties": { - "owner": { - "description": "Repository owner", - "type": "string" - }, - "repo": { - "description": "Repository name", - "type": "string" - }, - "run_id": { - "description": "The unique identifier of the workflow run", - "type": "number" - } - }, - "required": [ - "owner", - "repo", - "run_id" - ], - "type": "object" - }, - "name": "delete_workflow_run_logs" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/download_workflow_run_artifact.snap b/pkg/github/__toolsnaps__/download_workflow_run_artifact.snap deleted file mode 100644 index e831b21d5..000000000 --- a/pkg/github/__toolsnaps__/download_workflow_run_artifact.snap +++ /dev/null @@ -1,30 +0,0 @@ -{ - "annotations": { - "readOnlyHint": true, - "title": "Download workflow artifact" - }, - "description": "Get download URL for a workflow run artifact", - "inputSchema": { - "properties": { - "artifact_id": { - "description": "The unique identifier of the artifact", - "type": "number" - }, - "owner": { - "description": "Repository owner", - "type": "string" - }, - "repo": { - "description": "Repository name", - "type": "string" - } - }, - "required": [ - "owner", - "repo", - "artifact_id" - ], - "type": "object" - }, - "name": "download_workflow_run_artifact" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_project.snap b/pkg/github/__toolsnaps__/get_project.snap deleted file mode 100644 index 6ff320fe8..000000000 --- a/pkg/github/__toolsnaps__/get_project.snap +++ /dev/null @@ -1,34 +0,0 @@ -{ - "annotations": { - "readOnlyHint": true, - "title": "Get project" - }, - "description": "Get Project for a user or org", - "inputSchema": { - "properties": { - "owner": { - "description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - "type": "string" - }, - "owner_type": { - "description": "Owner type", - "enum": [ - "user", - "org" - ], - "type": "string" - }, - "project_number": { - "description": "The project's number", - "type": "number" - } - }, - "required": [ - "project_number", - "owner_type", - "owner" - ], - "type": "object" - }, - "name": "get_project" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_project_field.snap b/pkg/github/__toolsnaps__/get_project_field.snap deleted file mode 100644 index 9d884a20f..000000000 --- a/pkg/github/__toolsnaps__/get_project_field.snap +++ /dev/null @@ -1,39 +0,0 @@ -{ - "annotations": { - "readOnlyHint": true, - "title": "Get project field" - }, - "description": "Get Project field for a user or org", - "inputSchema": { - "properties": { - "field_id": { - "description": "The field's id.", - "type": "number" - }, - "owner": { - "description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - "type": "string" - }, - "owner_type": { - "description": "Owner type", - "enum": [ - "user", - "org" - ], - "type": "string" - }, - "project_number": { - "description": "The project's number.", - "type": "number" - } - }, - "required": [ - "owner_type", - "owner", - "project_number", - "field_id" - ], - "type": "object" - }, - "name": "get_project_field" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_project_item.snap b/pkg/github/__toolsnaps__/get_project_item.snap deleted file mode 100644 index 202bcc53e..000000000 --- a/pkg/github/__toolsnaps__/get_project_item.snap +++ /dev/null @@ -1,46 +0,0 @@ -{ - "annotations": { - "readOnlyHint": true, - "title": "Get project item" - }, - "description": "Get a specific Project item for a user or org", - "inputSchema": { - "properties": { - "fields": { - "description": "Specific list of field IDs to include in the response (e.g. [\"102589\", \"985201\", \"169875\"]). If not provided, only the title field is included.", - "items": { - "type": "string" - }, - "type": "array" - }, - "item_id": { - "description": "The item's ID.", - "type": "number" - }, - "owner": { - "description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - "type": "string" - }, - "owner_type": { - "description": "Owner type", - "enum": [ - "user", - "org" - ], - "type": "string" - }, - "project_number": { - "description": "The project's number.", - "type": "number" - } - }, - "required": [ - "owner_type", - "owner", - "project_number", - "item_id" - ], - "type": "object" - }, - "name": "get_project_item" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_workflow_run.snap b/pkg/github/__toolsnaps__/get_workflow_run.snap deleted file mode 100644 index e58ea0ba2..000000000 --- a/pkg/github/__toolsnaps__/get_workflow_run.snap +++ /dev/null @@ -1,30 +0,0 @@ -{ - "annotations": { - "readOnlyHint": true, - "title": "Get workflow run" - }, - "description": "Get details of a specific workflow run", - "inputSchema": { - "properties": { - "owner": { - "description": "Repository owner", - "type": "string" - }, - "repo": { - "description": "Repository name", - "type": "string" - }, - "run_id": { - "description": "The unique identifier of the workflow run", - "type": "number" - } - }, - "required": [ - "owner", - "repo", - "run_id" - ], - "type": "object" - }, - "name": "get_workflow_run" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_workflow_run_logs.snap b/pkg/github/__toolsnaps__/get_workflow_run_logs.snap deleted file mode 100644 index 8e76fbfc3..000000000 --- a/pkg/github/__toolsnaps__/get_workflow_run_logs.snap +++ /dev/null @@ -1,30 +0,0 @@ -{ - "annotations": { - "readOnlyHint": true, - "title": "Get workflow run logs" - }, - "description": "Download logs for a specific workflow run (EXPENSIVE: downloads ALL logs as ZIP. Consider using get_job_logs with failed_only=true for debugging failed jobs)", - "inputSchema": { - "properties": { - "owner": { - "description": "Repository owner", - "type": "string" - }, - "repo": { - "description": "Repository name", - "type": "string" - }, - "run_id": { - "description": "The unique identifier of the workflow run", - "type": "number" - } - }, - "required": [ - "owner", - "repo", - "run_id" - ], - "type": "object" - }, - "name": "get_workflow_run_logs" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_workflow_run_usage.snap b/pkg/github/__toolsnaps__/get_workflow_run_usage.snap deleted file mode 100644 index 40069b836..000000000 --- a/pkg/github/__toolsnaps__/get_workflow_run_usage.snap +++ /dev/null @@ -1,30 +0,0 @@ -{ - "annotations": { - "readOnlyHint": true, - "title": "Get workflow usage" - }, - "description": "Get usage metrics for a workflow run", - "inputSchema": { - "properties": { - "owner": { - "description": "Repository owner", - "type": "string" - }, - "repo": { - "description": "Repository name", - "type": "string" - }, - "run_id": { - "description": "The unique identifier of the workflow run", - "type": "number" - } - }, - "required": [ - "owner", - "repo", - "run_id" - ], - "type": "object" - }, - "name": "get_workflow_run_usage" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_project_fields.snap b/pkg/github/__toolsnaps__/list_project_fields.snap deleted file mode 100644 index 5456388b2..000000000 --- a/pkg/github/__toolsnaps__/list_project_fields.snap +++ /dev/null @@ -1,46 +0,0 @@ -{ - "annotations": { - "readOnlyHint": true, - "title": "List project fields" - }, - "description": "List Project fields for a user or org", - "inputSchema": { - "properties": { - "after": { - "description": "Forward pagination cursor from previous pageInfo.nextCursor.", - "type": "string" - }, - "before": { - "description": "Backward pagination cursor from previous pageInfo.prevCursor (rare).", - "type": "string" - }, - "owner": { - "description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - "type": "string" - }, - "owner_type": { - "description": "Owner type", - "enum": [ - "user", - "org" - ], - "type": "string" - }, - "per_page": { - "description": "Results per page (max 50)", - "type": "number" - }, - "project_number": { - "description": "The project's number.", - "type": "number" - } - }, - "required": [ - "owner_type", - "owner", - "project_number" - ], - "type": "object" - }, - "name": "list_project_fields" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_project_items.snap b/pkg/github/__toolsnaps__/list_project_items.snap deleted file mode 100644 index 5089f4306..000000000 --- a/pkg/github/__toolsnaps__/list_project_items.snap +++ /dev/null @@ -1,57 +0,0 @@ -{ - "annotations": { - "readOnlyHint": true, - "title": "List project items" - }, - "description": "Search project items with advanced filtering", - "inputSchema": { - "properties": { - "after": { - "description": "Forward pagination cursor from previous pageInfo.nextCursor.", - "type": "string" - }, - "before": { - "description": "Backward pagination cursor from previous pageInfo.prevCursor (rare).", - "type": "string" - }, - "fields": { - "description": "Field IDs to include (e.g. [\"102589\", \"985201\"]). CRITICAL: Always provide to get field values. Without this, only titles returned.", - "items": { - "type": "string" - }, - "type": "array" - }, - "owner": { - "description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - "type": "string" - }, - "owner_type": { - "description": "Owner type", - "enum": [ - "user", - "org" - ], - "type": "string" - }, - "per_page": { - "description": "Results per page (max 50)", - "type": "number" - }, - "project_number": { - "description": "The project's number.", - "type": "number" - }, - "query": { - "description": "Query string for advanced filtering of project items using GitHub's project filtering syntax.", - "type": "string" - } - }, - "required": [ - "owner_type", - "owner", - "project_number" - ], - "type": "object" - }, - "name": "list_project_items" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_projects.snap b/pkg/github/__toolsnaps__/list_projects.snap deleted file mode 100644 index be5a6713e..000000000 --- a/pkg/github/__toolsnaps__/list_projects.snap +++ /dev/null @@ -1,45 +0,0 @@ -{ - "annotations": { - "readOnlyHint": true, - "title": "List projects" - }, - "description": "List Projects for a user or organization", - "inputSchema": { - "properties": { - "after": { - "description": "Forward pagination cursor from previous pageInfo.nextCursor.", - "type": "string" - }, - "before": { - "description": "Backward pagination cursor from previous pageInfo.prevCursor (rare).", - "type": "string" - }, - "owner": { - "description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - "type": "string" - }, - "owner_type": { - "description": "Owner type", - "enum": [ - "user", - "org" - ], - "type": "string" - }, - "per_page": { - "description": "Results per page (max 50)", - "type": "number" - }, - "query": { - "description": "Filter projects by title text and open/closed state; permitted qualifiers: is:open, is:closed; examples: \"roadmap is:open\", \"is:open feature planning\".", - "type": "string" - } - }, - "required": [ - "owner_type", - "owner" - ], - "type": "object" - }, - "name": "list_projects" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_workflow_jobs.snap b/pkg/github/__toolsnaps__/list_workflow_jobs.snap deleted file mode 100644 index d8fed1965..000000000 --- a/pkg/github/__toolsnaps__/list_workflow_jobs.snap +++ /dev/null @@ -1,49 +0,0 @@ -{ - "annotations": { - "readOnlyHint": true, - "title": "List workflow jobs" - }, - "description": "List jobs for a specific workflow run", - "inputSchema": { - "properties": { - "filter": { - "description": "Filters jobs by their completed_at timestamp", - "enum": [ - "latest", - "all" - ], - "type": "string" - }, - "owner": { - "description": "Repository owner", - "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" - }, - "repo": { - "description": "Repository name", - "type": "string" - }, - "run_id": { - "description": "The unique identifier of the workflow run", - "type": "number" - } - }, - "required": [ - "owner", - "repo", - "run_id" - ], - "type": "object" - }, - "name": "list_workflow_jobs" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_workflow_run_artifacts.snap b/pkg/github/__toolsnaps__/list_workflow_run_artifacts.snap deleted file mode 100644 index 664722901..000000000 --- a/pkg/github/__toolsnaps__/list_workflow_run_artifacts.snap +++ /dev/null @@ -1,41 +0,0 @@ -{ - "annotations": { - "readOnlyHint": true, - "title": "List workflow artifacts" - }, - "description": "List artifacts for a workflow run", - "inputSchema": { - "properties": { - "owner": { - "description": "Repository owner", - "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" - }, - "repo": { - "description": "Repository name", - "type": "string" - }, - "run_id": { - "description": "The unique identifier of the workflow run", - "type": "number" - } - }, - "required": [ - "owner", - "repo", - "run_id" - ], - "type": "object" - }, - "name": "list_workflow_run_artifacts" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_workflow_runs.snap b/pkg/github/__toolsnaps__/list_workflow_runs.snap deleted file mode 100644 index a9a9916c3..000000000 --- a/pkg/github/__toolsnaps__/list_workflow_runs.snap +++ /dev/null @@ -1,98 +0,0 @@ -{ - "annotations": { - "readOnlyHint": true, - "title": "List workflow runs" - }, - "description": "List workflow runs for a specific workflow", - "inputSchema": { - "properties": { - "actor": { - "description": "Returns someone's workflow runs. Use the login for the user who created the workflow run.", - "type": "string" - }, - "branch": { - "description": "Returns workflow runs associated with a branch. Use the name of the branch.", - "type": "string" - }, - "event": { - "description": "Returns workflow runs for a specific event type", - "enum": [ - "branch_protection_rule", - "check_run", - "check_suite", - "create", - "delete", - "deployment", - "deployment_status", - "discussion", - "discussion_comment", - "fork", - "gollum", - "issue_comment", - "issues", - "label", - "merge_group", - "milestone", - "page_build", - "public", - "pull_request", - "pull_request_review", - "pull_request_review_comment", - "pull_request_target", - "push", - "registry_package", - "release", - "repository_dispatch", - "schedule", - "status", - "watch", - "workflow_call", - "workflow_dispatch", - "workflow_run" - ], - "type": "string" - }, - "owner": { - "description": "Repository owner", - "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" - }, - "repo": { - "description": "Repository name", - "type": "string" - }, - "status": { - "description": "Returns workflow runs with the check run status", - "enum": [ - "queued", - "in_progress", - "completed", - "requested", - "waiting" - ], - "type": "string" - }, - "workflow_id": { - "description": "The workflow ID or workflow file name", - "type": "string" - } - }, - "required": [ - "owner", - "repo", - "workflow_id" - ], - "type": "object" - }, - "name": "list_workflow_runs" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/list_workflows.snap b/pkg/github/__toolsnaps__/list_workflows.snap deleted file mode 100644 index b0e51e03a..000000000 --- a/pkg/github/__toolsnaps__/list_workflows.snap +++ /dev/null @@ -1,36 +0,0 @@ -{ - "annotations": { - "readOnlyHint": true, - "title": "List workflows" - }, - "description": "List workflows in a repository", - "inputSchema": { - "properties": { - "owner": { - "description": "Repository owner", - "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" - }, - "repo": { - "description": "Repository name", - "type": "string" - } - }, - "required": [ - "owner", - "repo" - ], - "type": "object" - }, - "name": "list_workflows" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/rerun_failed_jobs.snap b/pkg/github/__toolsnaps__/rerun_failed_jobs.snap deleted file mode 100644 index 099c89153..000000000 --- a/pkg/github/__toolsnaps__/rerun_failed_jobs.snap +++ /dev/null @@ -1,29 +0,0 @@ -{ - "annotations": { - "title": "Rerun failed jobs" - }, - "description": "Re-run only the failed jobs in a workflow run", - "inputSchema": { - "properties": { - "owner": { - "description": "Repository owner", - "type": "string" - }, - "repo": { - "description": "Repository name", - "type": "string" - }, - "run_id": { - "description": "The unique identifier of the workflow run", - "type": "number" - } - }, - "required": [ - "owner", - "repo", - "run_id" - ], - "type": "object" - }, - "name": "rerun_failed_jobs" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/rerun_workflow_run.snap b/pkg/github/__toolsnaps__/rerun_workflow_run.snap deleted file mode 100644 index 946bd72f3..000000000 --- a/pkg/github/__toolsnaps__/rerun_workflow_run.snap +++ /dev/null @@ -1,29 +0,0 @@ -{ - "annotations": { - "title": "Rerun workflow run" - }, - "description": "Re-run an entire workflow run", - "inputSchema": { - "properties": { - "owner": { - "description": "Repository owner", - "type": "string" - }, - "repo": { - "description": "Repository name", - "type": "string" - }, - "run_id": { - "description": "The unique identifier of the workflow run", - "type": "number" - } - }, - "required": [ - "owner", - "repo", - "run_id" - ], - "type": "object" - }, - "name": "rerun_workflow_run" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/run_workflow.snap b/pkg/github/__toolsnaps__/run_workflow.snap deleted file mode 100644 index 1b6c8993e..000000000 --- a/pkg/github/__toolsnaps__/run_workflow.snap +++ /dev/null @@ -1,38 +0,0 @@ -{ - "annotations": { - "title": "Run workflow" - }, - "description": "Run an Actions workflow by workflow ID or filename", - "inputSchema": { - "properties": { - "inputs": { - "description": "Inputs the workflow accepts", - "type": "object" - }, - "owner": { - "description": "Repository owner", - "type": "string" - }, - "ref": { - "description": "The git reference for the workflow. The reference can be a branch or tag name.", - "type": "string" - }, - "repo": { - "description": "Repository name", - "type": "string" - }, - "workflow_id": { - "description": "The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml)", - "type": "string" - } - }, - "required": [ - "owner", - "repo", - "workflow_id", - "ref" - ], - "type": "object" - }, - "name": "run_workflow" -} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/update_project_item.snap b/pkg/github/__toolsnaps__/update_project_item.snap deleted file mode 100644 index 987590741..000000000 --- a/pkg/github/__toolsnaps__/update_project_item.snap +++ /dev/null @@ -1,43 +0,0 @@ -{ - "annotations": { - "title": "Update project item" - }, - "description": "Update a specific Project item for a user or org", - "inputSchema": { - "properties": { - "item_id": { - "description": "The unique identifier of the project item. This is not the issue or pull request ID.", - "type": "number" - }, - "owner": { - "description": "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - "type": "string" - }, - "owner_type": { - "description": "Owner type", - "enum": [ - "user", - "org" - ], - "type": "string" - }, - "project_number": { - "description": "The project's number.", - "type": "number" - }, - "updated_field": { - "description": "Object consisting of the ID of the project field to update and the new value for the field. To clear the field, set value to null. Example: {\"id\": 123456, \"value\": \"New Value\"}", - "type": "object" - } - }, - "required": [ - "owner_type", - "owner", - "project_number", - "item_id", - "updated_field" - ], - "type": "object" - }, - "name": "update_project_item" -} \ No newline at end of file diff --git a/pkg/github/actions.go b/pkg/github/actions.go index d3e5aad8e..d1d9a5aa4 100644 --- a/pkg/github/actions.go +++ b/pkg/github/actions.go @@ -26,10 +26,6 @@ const ( DescriptionRepositoryName = "Repository name" ) -// FeatureFlagHoldbackConsolidatedActions is the feature flag that, when enabled, reverts to -// individual actions tools instead of the consolidated actions tools. -const FeatureFlagHoldbackConsolidatedActions = "mcp_holdback_consolidated_actions" - // Method constants for consolidated actions tools const ( actionsMethodListWorkflows = "list_workflows" @@ -49,1394 +45,158 @@ const ( actionsMethodDeleteWorkflowRunLogs = "delete_workflow_run_logs" ) -// ListWorkflows creates a tool to list workflows in a repository -func ListWorkflows(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataActions, - mcp.Tool{ - Name: "list_workflows", - Description: t("TOOL_LIST_WORKFLOWS_DESCRIPTION", "List workflows in a repository"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_LIST_WORKFLOWS_USER_TITLE", "List workflows"), - ReadOnlyHint: true, - }, - InputSchema: WithPagination(&jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: DescriptionRepositoryOwner, - }, - "repo": { - Type: "string", - Description: DescriptionRepositoryName, - }, - }, - Required: []string{"owner", "repo"}, - }), - }, - []scopes.Scope{scopes.Repo}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil - } +// handleFailedJobLogs gets logs for all failed jobs in a workflow run +func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo string, runID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, any, error) { + // First, get all jobs for the workflow run + jobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, &github.ListWorkflowJobsOptions{ + Filter: "latest", + }) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow jobs", resp, err), nil, nil + } + defer func() { _ = resp.Body.Close() }() - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + // Filter for failed jobs + var failedJobs []*github.WorkflowJob + for _, job := range jobs.Jobs { + if job.GetConclusion() == "failure" { + failedJobs = append(failedJobs, job) + } + } - // Get optional pagination parameters - pagination, err := OptionalPaginationParams(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + if len(failedJobs) == 0 { + result := map[string]any{ + "message": "No failed jobs found in this workflow run", + "run_id": runID, + "total_jobs": len(jobs.Jobs), + "failed_jobs": 0, + } + r, _ := json.Marshal(result) + return utils.NewToolResultText(string(r)), nil, nil + } - // Set up list options - opts := &github.ListOptions{ - PerPage: pagination.PerPage, - Page: pagination.Page, + // Collect logs for all failed jobs + var logResults []map[string]any + for _, job := range failedJobs { + jobResult, resp, err := getJobLogData(ctx, client, owner, repo, job.GetID(), job.GetName(), returnContent, tailLines, contentWindowSize) + if err != nil { + // Continue with other jobs even if one fails + jobResult = map[string]any{ + "job_id": job.GetID(), + "job_name": job.GetName(), + "error": err.Error(), } + // Enable reporting of status codes and error causes + _, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get job logs", resp, err) // Explicitly ignore error for graceful handling + } - workflows, resp, err := client.Actions.ListWorkflows(ctx, owner, repo, opts) - if err != nil { - return nil, nil, fmt.Errorf("failed to list workflows: %w", err) - } - defer func() { _ = resp.Body.Close() }() + logResults = append(logResults, jobResult) + } - r, err := json.Marshal(workflows) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + result := map[string]any{ + "message": fmt.Sprintf("Retrieved logs for %d failed jobs", len(failedJobs)), + "run_id": runID, + "total_jobs": len(jobs.Jobs), + "failed_jobs": len(failedJobs), + "logs": logResults, + "return_format": map[string]bool{"content": returnContent, "urls": !returnContent}, + } - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedActions - return tool -} + r, err := json.Marshal(result) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } -// ListWorkflowRuns creates a tool to list workflow runs for a specific workflow -func ListWorkflowRuns(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataActions, - mcp.Tool{ - Name: "list_workflow_runs", - Description: t("TOOL_LIST_WORKFLOW_RUNS_DESCRIPTION", "List workflow runs for a specific workflow"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_LIST_WORKFLOW_RUNS_USER_TITLE", "List workflow runs"), - ReadOnlyHint: true, - }, - InputSchema: WithPagination(&jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: DescriptionRepositoryOwner, - }, - "repo": { - Type: "string", - Description: DescriptionRepositoryName, - }, - "workflow_id": { - Type: "string", - Description: "The workflow ID or workflow file name", - }, - "actor": { - Type: "string", - Description: "Returns someone's workflow runs. Use the login for the user who created the workflow run.", - }, - "branch": { - Type: "string", - Description: "Returns workflow runs associated with a branch. Use the name of the branch.", - }, - "event": { - Type: "string", - Description: "Returns workflow runs for a specific event type", - Enum: []any{ - "branch_protection_rule", - "check_run", - "check_suite", - "create", - "delete", - "deployment", - "deployment_status", - "discussion", - "discussion_comment", - "fork", - "gollum", - "issue_comment", - "issues", - "label", - "merge_group", - "milestone", - "page_build", - "public", - "pull_request", - "pull_request_review", - "pull_request_review_comment", - "pull_request_target", - "push", - "registry_package", - "release", - "repository_dispatch", - "schedule", - "status", - "watch", - "workflow_call", - "workflow_dispatch", - "workflow_run", - }, - }, - "status": { - Type: "string", - Description: "Returns workflow runs with the check run status", - Enum: []any{"queued", "in_progress", "completed", "requested", "waiting"}, - }, - }, - Required: []string{"owner", "repo", "workflow_id"}, - }), - }, - []scopes.Scope{scopes.Repo}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil - } + return utils.NewToolResultText(string(r)), nil, nil +} - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - workflowID, err := RequiredParam[string](args, "workflow_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } +// handleSingleJobLogs gets logs for a single job +func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo string, jobID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, any, error) { + jobResult, resp, err := getJobLogData(ctx, client, owner, repo, jobID, "", returnContent, tailLines, contentWindowSize) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get job logs", resp, err), nil, nil + } - // Get optional filtering parameters - actor, err := OptionalParam[string](args, "actor") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - branch, err := OptionalParam[string](args, "branch") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - event, err := OptionalParam[string](args, "event") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - status, err := OptionalParam[string](args, "status") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + r, err := json.Marshal(jobResult) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } - // Get optional pagination parameters - pagination, err := OptionalPaginationParams(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + return utils.NewToolResultText(string(r)), nil, nil +} - // Set up list options - opts := &github.ListWorkflowRunsOptions{ - Actor: actor, - Branch: branch, - Event: event, - Status: status, - ListOptions: github.ListOptions{ - PerPage: pagination.PerPage, - Page: pagination.Page, - }, - } +// getJobLogData retrieves log data for a single job, either as URL or content +func getJobLogData(ctx context.Context, client *github.Client, owner, repo string, jobID int64, jobName string, returnContent bool, tailLines int, contentWindowSize int) (map[string]any, *github.Response, error) { + // Get the download URL for the job logs + url, resp, err := client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 1) + if err != nil { + return nil, resp, fmt.Errorf("failed to get job logs for job %d: %w", jobID, err) + } + defer func() { _ = resp.Body.Close() }() - workflowRuns, resp, err := client.Actions.ListWorkflowRunsByFileName(ctx, owner, repo, workflowID, opts) - if err != nil { - return nil, nil, fmt.Errorf("failed to list workflow runs: %w", err) - } - defer func() { _ = resp.Body.Close() }() + result := map[string]any{ + "job_id": jobID, + } + if jobName != "" { + result["job_name"] = jobName + } - r, err := json.Marshal(workflowRuns) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + if returnContent { + // Download and return the actual log content + content, originalLength, httpResp, err := downloadLogContent(ctx, url.String(), tailLines, contentWindowSize) //nolint:bodyclose // Response body is closed in downloadLogContent, but we need to return httpResp + if err != nil { + // To keep the return value consistent wrap the response as a GitHub Response + ghRes := &github.Response{ + Response: httpResp, } + return nil, ghRes, fmt.Errorf("failed to download log content for job %d: %w", jobID, err) + } + result["logs_content"] = content + result["message"] = "Job logs content retrieved successfully" + result["original_length"] = originalLength + } else { + // Return just the URL + result["logs_url"] = url.String() + result["message"] = "Job logs are available for download" + result["note"] = "The logs_url provides a download link for the individual job logs in plain text format. Use return_content=true to get the actual log content." + } - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedActions - return tool + return result, resp, nil } -// RunWorkflow creates a tool to run an Actions workflow -func RunWorkflow(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataActions, - mcp.Tool{ - Name: "run_workflow", - Description: t("TOOL_RUN_WORKFLOW_DESCRIPTION", "Run an Actions workflow by workflow ID or filename"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_RUN_WORKFLOW_USER_TITLE", "Run workflow"), - ReadOnlyHint: false, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: DescriptionRepositoryOwner, - }, - "repo": { - Type: "string", - Description: DescriptionRepositoryName, - }, - "workflow_id": { - Type: "string", - Description: "The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml)", - }, - "ref": { - Type: "string", - Description: "The git reference for the workflow. The reference can be a branch or tag name.", - }, - "inputs": { - Type: "object", - Description: "Inputs the workflow accepts", - }, - }, - Required: []string{"owner", "repo", "workflow_id", "ref"}, - }, - }, - []scopes.Scope{scopes.Repo}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil - } +func downloadLogContent(ctx context.Context, logURL string, tailLines int, maxLines int) (string, int, *http.Response, error) { + prof := profiler.New(nil, profiler.IsProfilingEnabled()) + finish := prof.Start(ctx, "log_buffer_processing") - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - workflowID, err := RequiredParam[string](args, "workflow_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - ref, err := RequiredParam[string](args, "ref") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - // Get optional inputs parameter - var inputs map[string]interface{} - if requestInputs, ok := args["inputs"]; ok { - if inputsMap, ok := requestInputs.(map[string]interface{}); ok { - inputs = inputsMap - } - } - - event := github.CreateWorkflowDispatchEventRequest{ - Ref: ref, - Inputs: inputs, - } - - var resp *github.Response - var workflowType string - - if workflowIDInt, parseErr := strconv.ParseInt(workflowID, 10, 64); parseErr == nil { - resp, err = client.Actions.CreateWorkflowDispatchEventByID(ctx, owner, repo, workflowIDInt, event) - workflowType = "workflow_id" - } else { - resp, err = client.Actions.CreateWorkflowDispatchEventByFileName(ctx, owner, repo, workflowID, event) - workflowType = "workflow_file" - } - - if err != nil { - return nil, nil, fmt.Errorf("failed to run workflow: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - result := map[string]any{ - "message": "Workflow run has been queued", - "workflow_type": workflowType, - "workflow_id": workflowID, - "ref": ref, - "inputs": inputs, - "status": resp.Status, - "status_code": resp.StatusCode, - } - - 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 - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedActions - return tool -} - -// GetWorkflowRun creates a tool to get details of a specific workflow run -func GetWorkflowRun(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataActions, - mcp.Tool{ - Name: "get_workflow_run", - Description: t("TOOL_GET_WORKFLOW_RUN_DESCRIPTION", "Get details of a specific workflow run"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_WORKFLOW_RUN_USER_TITLE", "Get workflow run"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: DescriptionRepositoryOwner, - }, - "repo": { - Type: "string", - Description: DescriptionRepositoryName, - }, - "run_id": { - Type: "number", - Description: "The unique identifier of the workflow run", - }, - }, - Required: []string{"owner", "repo", "run_id"}, - }, - }, - []scopes.Scope{scopes.Repo}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil - } - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runIDInt, err := RequiredInt(args, "run_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runID := int64(runIDInt) - - workflowRun, resp, err := client.Actions.GetWorkflowRunByID(ctx, owner, repo, runID) - if err != nil { - return nil, nil, fmt.Errorf("failed to get workflow run: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - r, err := json.Marshal(workflowRun) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedActions - return tool -} - -// GetWorkflowRunLogs creates a tool to download logs for a specific workflow run -func GetWorkflowRunLogs(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataActions, - mcp.Tool{ - Name: "get_workflow_run_logs", - Description: t("TOOL_GET_WORKFLOW_RUN_LOGS_DESCRIPTION", "Download logs for a specific workflow run (EXPENSIVE: downloads ALL logs as ZIP. Consider using get_job_logs with failed_only=true for debugging failed jobs)"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_WORKFLOW_RUN_LOGS_USER_TITLE", "Get workflow run logs"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: DescriptionRepositoryOwner, - }, - "repo": { - Type: "string", - Description: DescriptionRepositoryName, - }, - "run_id": { - Type: "number", - Description: "The unique identifier of the workflow run", - }, - }, - Required: []string{"owner", "repo", "run_id"}, - }, - }, - []scopes.Scope{scopes.Repo}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil - } - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runIDInt, err := RequiredInt(args, "run_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runID := int64(runIDInt) - - // Get the download URL for the logs - url, resp, err := client.Actions.GetWorkflowRunLogs(ctx, owner, repo, runID, 1) - if err != nil { - return nil, nil, fmt.Errorf("failed to get workflow run logs: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - // Create response with the logs URL and information - result := map[string]any{ - "logs_url": url.String(), - "message": "Workflow run logs are available for download", - "note": "The logs_url provides a download link for the complete workflow run logs as a ZIP archive. You can download this archive to extract and examine individual job logs.", - "warning": "This downloads ALL logs as a ZIP file which can be large and expensive. For debugging failed jobs, consider using get_job_logs with failed_only=true and run_id instead.", - "optimization_tip": "Use: get_job_logs with parameters {run_id: " + fmt.Sprintf("%d", runID) + ", failed_only: true} for more efficient failed job debugging", - } - - 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 - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedActions - return tool -} - -// ListWorkflowJobs creates a tool to list jobs for a specific workflow run -func ListWorkflowJobs(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataActions, - mcp.Tool{ - Name: "list_workflow_jobs", - Description: t("TOOL_LIST_WORKFLOW_JOBS_DESCRIPTION", "List jobs for a specific workflow run"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_LIST_WORKFLOW_JOBS_USER_TITLE", "List workflow jobs"), - ReadOnlyHint: true, - }, - InputSchema: WithPagination(&jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: DescriptionRepositoryOwner, - }, - "repo": { - Type: "string", - Description: DescriptionRepositoryName, - }, - "run_id": { - Type: "number", - Description: "The unique identifier of the workflow run", - }, - "filter": { - Type: "string", - Description: "Filters jobs by their completed_at timestamp", - Enum: []any{"latest", "all"}, - }, - }, - Required: []string{"owner", "repo", "run_id"}, - }), - }, - []scopes.Scope{scopes.Repo}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil - } - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runIDInt, err := RequiredInt(args, "run_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runID := int64(runIDInt) - - // Get optional filtering parameters - filter, err := OptionalParam[string](args, "filter") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - // Get optional pagination parameters - pagination, err := OptionalPaginationParams(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - // Set up list options - opts := &github.ListWorkflowJobsOptions{ - Filter: filter, - ListOptions: github.ListOptions{ - PerPage: pagination.PerPage, - Page: pagination.Page, - }, - } - - jobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, opts) - if err != nil { - return nil, nil, fmt.Errorf("failed to list workflow jobs: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - // Add optimization tip for failed job debugging - response := map[string]any{ - "jobs": jobs, - "optimization_tip": "For debugging failed jobs, consider using get_job_logs with failed_only=true and run_id=" + fmt.Sprintf("%d", runID) + " to get logs directly without needing to list jobs first", - } - - r, err := json.Marshal(response) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedActions - return tool -} - -// GetJobLogs creates a tool to download logs for a specific workflow job or efficiently get all failed job logs for a workflow run -func GetJobLogs(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataActions, - mcp.Tool{ - Name: "get_job_logs", - Description: t("TOOL_GET_JOB_LOGS_DESCRIPTION", "Download logs for a specific workflow job or efficiently get all failed job logs for a workflow run"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_JOB_LOGS_USER_TITLE", "Get job logs"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: DescriptionRepositoryOwner, - }, - "repo": { - Type: "string", - Description: DescriptionRepositoryName, - }, - "job_id": { - Type: "number", - Description: "The unique identifier of the workflow job (required for single job logs)", - }, - "run_id": { - Type: "number", - Description: "Workflow run ID (required when using failed_only)", - }, - "failed_only": { - Type: "boolean", - Description: "When true, gets logs for all failed jobs in run_id", - }, - "return_content": { - Type: "boolean", - Description: "Returns actual log content instead of URLs", - }, - "tail_lines": { - Type: "number", - Description: "Number of lines to return from the end of the log", - Default: json.RawMessage(`500`), - }, - }, - Required: []string{"owner", "repo"}, - }, - }, - []scopes.Scope{scopes.Repo}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil - } - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - // Get optional parameters - jobID, err := OptionalIntParam(args, "job_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runID, err := OptionalIntParam(args, "run_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - failedOnly, err := OptionalParam[bool](args, "failed_only") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - returnContent, err := OptionalParam[bool](args, "return_content") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - tailLines, err := OptionalIntParam(args, "tail_lines") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - // Default to 500 lines if not specified - if tailLines == 0 { - tailLines = 500 - } - - // Validate parameters - if failedOnly && runID == 0 { - return utils.NewToolResultError("run_id is required when failed_only is true"), nil, nil - } - if !failedOnly && jobID == 0 { - return utils.NewToolResultError("job_id is required when failed_only is false"), nil, nil - } - - if failedOnly && runID > 0 { - // Handle failed-only mode: get logs for all failed jobs in the workflow run - return handleFailedJobLogs(ctx, client, owner, repo, int64(runID), returnContent, tailLines, deps.GetContentWindowSize()) - } else if jobID > 0 { - // Handle single job mode - return handleSingleJobLogs(ctx, client, owner, repo, int64(jobID), returnContent, tailLines, deps.GetContentWindowSize()) - } - - return utils.NewToolResultError("Either job_id must be provided for single job logs, or run_id with failed_only=true for failed job logs"), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedActions - return tool -} - -// handleFailedJobLogs gets logs for all failed jobs in a workflow run -func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo string, runID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, any, error) { - // First, get all jobs for the workflow run - jobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, &github.ListWorkflowJobsOptions{ - Filter: "latest", - }) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow jobs", resp, err), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - // Filter for failed jobs - var failedJobs []*github.WorkflowJob - for _, job := range jobs.Jobs { - if job.GetConclusion() == "failure" { - failedJobs = append(failedJobs, job) - } - } - - if len(failedJobs) == 0 { - result := map[string]any{ - "message": "No failed jobs found in this workflow run", - "run_id": runID, - "total_jobs": len(jobs.Jobs), - "failed_jobs": 0, - } - r, _ := json.Marshal(result) - return utils.NewToolResultText(string(r)), nil, nil - } - - // Collect logs for all failed jobs - var logResults []map[string]any - for _, job := range failedJobs { - jobResult, resp, err := getJobLogData(ctx, client, owner, repo, job.GetID(), job.GetName(), returnContent, tailLines, contentWindowSize) - if err != nil { - // Continue with other jobs even if one fails - jobResult = map[string]any{ - "job_id": job.GetID(), - "job_name": job.GetName(), - "error": err.Error(), - } - // Enable reporting of status codes and error causes - _, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get job logs", resp, err) // Explicitly ignore error for graceful handling - } - - logResults = append(logResults, jobResult) - } - - result := map[string]any{ - "message": fmt.Sprintf("Retrieved logs for %d failed jobs", len(failedJobs)), - "run_id": runID, - "total_jobs": len(jobs.Jobs), - "failed_jobs": len(failedJobs), - "logs": logResults, - "return_format": map[string]bool{"content": returnContent, "urls": !returnContent}, - } - - 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 -} - -// handleSingleJobLogs gets logs for a single job -func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo string, jobID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, any, error) { - jobResult, resp, err := getJobLogData(ctx, client, owner, repo, jobID, "", returnContent, tailLines, contentWindowSize) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get job logs", resp, err), nil, nil - } - - r, err := json.Marshal(jobResult) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil, nil -} - -// getJobLogData retrieves log data for a single job, either as URL or content -func getJobLogData(ctx context.Context, client *github.Client, owner, repo string, jobID int64, jobName string, returnContent bool, tailLines int, contentWindowSize int) (map[string]any, *github.Response, error) { - // Get the download URL for the job logs - url, resp, err := client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 1) - if err != nil { - return nil, resp, fmt.Errorf("failed to get job logs for job %d: %w", jobID, err) - } - defer func() { _ = resp.Body.Close() }() - - result := map[string]any{ - "job_id": jobID, - } - if jobName != "" { - result["job_name"] = jobName - } - - if returnContent { - // Download and return the actual log content - content, originalLength, httpResp, err := downloadLogContent(ctx, url.String(), tailLines, contentWindowSize) //nolint:bodyclose // Response body is closed in downloadLogContent, but we need to return httpResp - if err != nil { - // To keep the return value consistent wrap the response as a GitHub Response - ghRes := &github.Response{ - Response: httpResp, - } - return nil, ghRes, fmt.Errorf("failed to download log content for job %d: %w", jobID, err) - } - result["logs_content"] = content - result["message"] = "Job logs content retrieved successfully" - result["original_length"] = originalLength - } else { - // Return just the URL - result["logs_url"] = url.String() - result["message"] = "Job logs are available for download" - result["note"] = "The logs_url provides a download link for the individual job logs in plain text format. Use return_content=true to get the actual log content." - } - - return result, resp, nil -} - -func downloadLogContent(ctx context.Context, logURL string, tailLines int, maxLines int) (string, int, *http.Response, error) { - prof := profiler.New(nil, profiler.IsProfilingEnabled()) - finish := prof.Start(ctx, "log_buffer_processing") - - httpResp, err := http.Get(logURL) //nolint:gosec - if err != nil { - return "", 0, httpResp, fmt.Errorf("failed to download logs: %w", err) - } - defer func() { _ = httpResp.Body.Close() }() - - if httpResp.StatusCode != http.StatusOK { - return "", 0, httpResp, fmt.Errorf("failed to download logs: HTTP %d", httpResp.StatusCode) - } - - bufferSize := tailLines - if bufferSize > maxLines { - bufferSize = maxLines - } - - processedInput, totalLines, httpResp, err := buffer.ProcessResponseAsRingBufferToEnd(httpResp, bufferSize) - if err != nil { - return "", 0, httpResp, fmt.Errorf("failed to process log content: %w", err) - } - - lines := strings.Split(processedInput, "\n") - if len(lines) > tailLines { - lines = lines[len(lines)-tailLines:] - } - finalResult := strings.Join(lines, "\n") - - _ = finish(len(lines), int64(len(finalResult))) - - return finalResult, totalLines, httpResp, nil -} - -// RerunWorkflowRun creates a tool to re-run an entire workflow run -func RerunWorkflowRun(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataActions, - mcp.Tool{ - Name: "rerun_workflow_run", - Description: t("TOOL_RERUN_WORKFLOW_RUN_DESCRIPTION", "Re-run an entire workflow run"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_RERUN_WORKFLOW_RUN_USER_TITLE", "Rerun workflow run"), - ReadOnlyHint: false, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: DescriptionRepositoryOwner, - }, - "repo": { - Type: "string", - Description: DescriptionRepositoryName, - }, - "run_id": { - Type: "number", - Description: "The unique identifier of the workflow run", - }, - }, - Required: []string{"owner", "repo", "run_id"}, - }, - }, - []scopes.Scope{scopes.Repo}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil - } - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runIDInt, err := RequiredInt(args, "run_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runID := int64(runIDInt) - - resp, err := client.Actions.RerunWorkflowByID(ctx, owner, repo, runID) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to rerun workflow run", resp, err), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - result := map[string]any{ - "message": "Workflow run has been queued for re-run", - "run_id": runID, - "status": resp.Status, - "status_code": resp.StatusCode, - } - - 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 - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedActions - return tool -} - -// RerunFailedJobs creates a tool to re-run only the failed jobs in a workflow run -func RerunFailedJobs(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataActions, - mcp.Tool{ - Name: "rerun_failed_jobs", - Description: t("TOOL_RERUN_FAILED_JOBS_DESCRIPTION", "Re-run only the failed jobs in a workflow run"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_RERUN_FAILED_JOBS_USER_TITLE", "Rerun failed jobs"), - ReadOnlyHint: false, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: DescriptionRepositoryOwner, - }, - "repo": { - Type: "string", - Description: DescriptionRepositoryName, - }, - "run_id": { - Type: "number", - Description: "The unique identifier of the workflow run", - }, - }, - Required: []string{"owner", "repo", "run_id"}, - }, - }, - []scopes.Scope{scopes.Repo}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil - } - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runIDInt, err := RequiredInt(args, "run_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runID := int64(runIDInt) - - resp, err := client.Actions.RerunFailedJobsByID(ctx, owner, repo, runID) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to rerun failed jobs", resp, err), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - result := map[string]any{ - "message": "Failed jobs have been queued for re-run", - "run_id": runID, - "status": resp.Status, - "status_code": resp.StatusCode, - } - - 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 - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedActions - return tool -} - -// CancelWorkflowRun creates a tool to cancel a workflow run -func CancelWorkflowRun(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataActions, - mcp.Tool{ - Name: "cancel_workflow_run", - Description: t("TOOL_CANCEL_WORKFLOW_RUN_DESCRIPTION", "Cancel a workflow run"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_CANCEL_WORKFLOW_RUN_USER_TITLE", "Cancel workflow run"), - ReadOnlyHint: false, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: DescriptionRepositoryOwner, - }, - "repo": { - Type: "string", - Description: DescriptionRepositoryName, - }, - "run_id": { - Type: "number", - Description: "The unique identifier of the workflow run", - }, - }, - Required: []string{"owner", "repo", "run_id"}, - }, - }, - []scopes.Scope{scopes.Repo}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil - } - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runIDInt, err := RequiredInt(args, "run_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runID := int64(runIDInt) - - resp, err := client.Actions.CancelWorkflowRunByID(ctx, owner, repo, runID) - if err != nil { - if _, ok := err.(*github.AcceptedError); !ok { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to cancel workflow run", resp, err), nil, nil - } - } - defer func() { _ = resp.Body.Close() }() - - result := map[string]any{ - "message": "Workflow run has been cancelled", - "run_id": runID, - "status": resp.Status, - "status_code": resp.StatusCode, - } - - 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 - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedActions - return tool -} - -// ListWorkflowRunArtifacts creates a tool to list artifacts for a workflow run -func ListWorkflowRunArtifacts(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataActions, - mcp.Tool{ - Name: "list_workflow_run_artifacts", - Description: t("TOOL_LIST_WORKFLOW_RUN_ARTIFACTS_DESCRIPTION", "List artifacts for a workflow run"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_LIST_WORKFLOW_RUN_ARTIFACTS_USER_TITLE", "List workflow artifacts"), - ReadOnlyHint: true, - }, - InputSchema: WithPagination(&jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: DescriptionRepositoryOwner, - }, - "repo": { - Type: "string", - Description: DescriptionRepositoryName, - }, - "run_id": { - Type: "number", - Description: "The unique identifier of the workflow run", - }, - }, - Required: []string{"owner", "repo", "run_id"}, - }), - }, - []scopes.Scope{scopes.Repo}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil - } - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runIDInt, err := RequiredInt(args, "run_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runID := int64(runIDInt) - - // Get optional pagination parameters - pagination, err := OptionalPaginationParams(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - // Set up list options - opts := &github.ListOptions{ - PerPage: pagination.PerPage, - Page: pagination.Page, - } - - artifacts, resp, err := client.Actions.ListWorkflowRunArtifacts(ctx, owner, repo, runID, opts) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow run artifacts", resp, err), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - r, err := json.Marshal(artifacts) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedActions - return tool -} - -// DownloadWorkflowRunArtifact creates a tool to download a workflow run artifact -func DownloadWorkflowRunArtifact(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataActions, - mcp.Tool{ - Name: "download_workflow_run_artifact", - Description: t("TOOL_DOWNLOAD_WORKFLOW_RUN_ARTIFACT_DESCRIPTION", "Get download URL for a workflow run artifact"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_DOWNLOAD_WORKFLOW_RUN_ARTIFACT_USER_TITLE", "Download workflow artifact"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: DescriptionRepositoryOwner, - }, - "repo": { - Type: "string", - Description: DescriptionRepositoryName, - }, - "artifact_id": { - Type: "number", - Description: "The unique identifier of the artifact", - }, - }, - Required: []string{"owner", "repo", "artifact_id"}, - }, - }, - []scopes.Scope{scopes.Repo}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil - } - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - artifactIDInt, err := RequiredInt(args, "artifact_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - artifactID := int64(artifactIDInt) - - // Get the download URL for the artifact - url, resp, err := client.Actions.DownloadArtifact(ctx, owner, repo, artifactID, 1) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get artifact download URL", resp, err), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - // Create response with the download URL and information - result := map[string]any{ - "download_url": url.String(), - "message": "Artifact is available for download", - "note": "The download_url provides a download link for the artifact as a ZIP archive. The link is temporary and expires after a short time.", - "artifact_id": artifactID, - } - - 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 - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedActions - return tool -} - -// DeleteWorkflowRunLogs creates a tool to delete logs for a workflow run -func DeleteWorkflowRunLogs(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataActions, - mcp.Tool{ - Name: "delete_workflow_run_logs", - Description: t("TOOL_DELETE_WORKFLOW_RUN_LOGS_DESCRIPTION", "Delete logs for a workflow run"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_DELETE_WORKFLOW_RUN_LOGS_USER_TITLE", "Delete workflow logs"), - ReadOnlyHint: false, - DestructiveHint: jsonschema.Ptr(true), - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: DescriptionRepositoryOwner, - }, - "repo": { - Type: "string", - Description: DescriptionRepositoryName, - }, - "run_id": { - Type: "number", - Description: "The unique identifier of the workflow run", - }, - }, - Required: []string{"owner", "repo", "run_id"}, - }, - }, - []scopes.Scope{scopes.Repo}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil - } - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runIDInt, err := RequiredInt(args, "run_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runID := int64(runIDInt) - - resp, err := client.Actions.DeleteWorkflowRunLogs(ctx, owner, repo, runID) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to delete workflow run logs", resp, err), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - result := map[string]any{ - "message": "Workflow run logs have been deleted", - "run_id": runID, - "status": resp.Status, - "status_code": resp.StatusCode, - } - - r, err := json.Marshal(result) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + httpResp, err := http.Get(logURL) //nolint:gosec + if err != nil { + return "", 0, httpResp, fmt.Errorf("failed to download logs: %w", err) + } + defer func() { _ = httpResp.Body.Close() }() - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedActions - return tool -} + if httpResp.StatusCode != http.StatusOK { + return "", 0, httpResp, fmt.Errorf("failed to download logs: HTTP %d", httpResp.StatusCode) + } -// GetWorkflowRunUsage creates a tool to get usage metrics for a workflow run -func GetWorkflowRunUsage(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataActions, - mcp.Tool{ - Name: "get_workflow_run_usage", - Description: t("TOOL_GET_WORKFLOW_RUN_USAGE_DESCRIPTION", "Get usage metrics for a workflow run"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_WORKFLOW_RUN_USAGE_USER_TITLE", "Get workflow usage"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner": { - Type: "string", - Description: DescriptionRepositoryOwner, - }, - "repo": { - Type: "string", - Description: DescriptionRepositoryName, - }, - "run_id": { - Type: "number", - Description: "The unique identifier of the workflow run", - }, - }, - Required: []string{"owner", "repo", "run_id"}, - }, - }, - []scopes.Scope{scopes.Repo}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil - } + bufferSize := tailLines + if bufferSize > maxLines { + bufferSize = maxLines + } - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runIDInt, err := RequiredInt(args, "run_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - runID := int64(runIDInt) + processedInput, totalLines, httpResp, err := buffer.ProcessResponseAsRingBufferToEnd(httpResp, bufferSize) + if err != nil { + return "", 0, httpResp, fmt.Errorf("failed to process log content: %w", err) + } - usage, resp, err := client.Actions.GetWorkflowRunUsageByID(ctx, owner, repo, runID) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get workflow run usage", resp, err), nil, nil - } - defer func() { _ = resp.Body.Close() }() + lines := strings.Split(processedInput, "\n") + if len(lines) > tailLines { + lines = lines[len(lines)-tailLines:] + } + finalResult := strings.Join(lines, "\n") - r, err := json.Marshal(usage) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } + _ = finish(len(lines), int64(len(finalResult))) - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedActions - return tool + return finalResult, totalLines, httpResp, nil } // ActionsList returns the tool and handler for listing GitHub Actions resources. @@ -1631,7 +391,6 @@ Use this tool to list workflows in a repository, or list workflow runs, jobs, an } }, ) - tool.FeatureFlagDisable = FeatureFlagHoldbackConsolidatedActions return tool } @@ -1740,7 +499,6 @@ Use this tool to get details about individual workflows, workflow runs, jobs, an } }, ) - tool.FeatureFlagDisable = FeatureFlagHoldbackConsolidatedActions return tool } @@ -1859,7 +617,6 @@ func ActionsRunTrigger(t translations.TranslationHelperFunc) inventory.ServerToo } }, ) - tool.FeatureFlagDisable = FeatureFlagHoldbackConsolidatedActions return tool } @@ -1977,7 +734,6 @@ For single job logs, provide job_id. For all failed jobs in a run, provide run_i return utils.NewToolResultError("Either job_id must be provided for single job logs, or run_id with failed_only=true for failed job logs"), nil, nil }, ) - tool.FeatureFlagDisable = FeatureFlagHoldbackConsolidatedActions return tool } diff --git a/pkg/github/actions_test.go b/pkg/github/actions_test.go index 0d47236f6..fe0f5575d 100644 --- a/pkg/github/actions_test.go +++ b/pkg/github/actions_test.go @@ -3,18 +3,10 @@ package github import ( "context" "encoding/json" - "io" "net/http" - "net/http/httptest" - "os" - "runtime" - "runtime/debug" - "strings" "testing" - "github.com/github/github-mcp-server/internal/profiler" "github.com/github/github-mcp-server/internal/toolsnaps" - buffer "github.com/github/github-mcp-server/pkg/buffer" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v79/github" "github.com/google/jsonschema-go/jsonschema" @@ -22,1804 +14,6 @@ import ( "github.com/stretchr/testify/require" ) -func Test_ListWorkflows(t *testing.T) { - // Verify tool definition once - toolDef := ListWorkflows(translations.NullTranslationHelper) - require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) - - assert.Equal(t, "list_workflows", toolDef.Tool.Name) - assert.NotEmpty(t, toolDef.Tool.Description) - inputSchema := toolDef.Tool.InputSchema.(*jsonschema.Schema) - assert.Contains(t, inputSchema.Properties, "owner") - assert.Contains(t, inputSchema.Properties, "repo") - assert.Contains(t, inputSchema.Properties, "perPage") - assert.Contains(t, inputSchema.Properties, "page") - assert.ElementsMatch(t, inputSchema.Required, []string{"owner", "repo"}) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - }{ - { - name: "successful workflow listing", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsWorkflowsByOwnerByRepo: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - workflows := &github.Workflows{ - TotalCount: github.Ptr(2), - Workflows: []*github.Workflow{ - { - ID: github.Ptr(int64(123)), - Name: github.Ptr("CI"), - Path: github.Ptr(".github/workflows/ci.yml"), - State: github.Ptr("active"), - CreatedAt: &github.Timestamp{}, - UpdatedAt: &github.Timestamp{}, - URL: github.Ptr("https://api.github.com/repos/owner/repo/actions/workflows/123"), - HTMLURL: github.Ptr("https://github.com/owner/repo/actions/workflows/ci.yml"), - BadgeURL: github.Ptr("https://github.com/owner/repo/workflows/CI/badge.svg"), - NodeID: github.Ptr("W_123"), - }, - { - ID: github.Ptr(int64(456)), - Name: github.Ptr("Deploy"), - Path: github.Ptr(".github/workflows/deploy.yml"), - State: github.Ptr("active"), - CreatedAt: &github.Timestamp{}, - UpdatedAt: &github.Timestamp{}, - URL: github.Ptr("https://api.github.com/repos/owner/repo/actions/workflows/456"), - HTMLURL: github.Ptr("https://github.com/owner/repo/actions/workflows/deploy.yml"), - BadgeURL: github.Ptr("https://github.com/owner/repo/workflows/Deploy/badge.svg"), - NodeID: github.Ptr("W_456"), - }, - }, - } - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(workflows) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - }, - expectError: false, - }, - { - name: "missing required parameter owner", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "repo": "repo", - }, - expectError: true, - expectedErrMsg: "missing required parameter: owner", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup client with mock - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := toolDef.Handler(deps) - - // Create call request - request := createMCPRequest(tc.requestArgs) - - // Call handler - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - // Parse the result and get the text content if no error - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Equal(t, tc.expectedErrMsg, textContent.Text) - return - } - - // Unmarshal and verify the result - var response github.Workflows - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - assert.NotNil(t, response.TotalCount) - assert.Greater(t, *response.TotalCount, 0) - assert.NotEmpty(t, response.Workflows) - }) - } -} - -func Test_RunWorkflow(t *testing.T) { - // Verify tool definition once - toolDef := RunWorkflow(translations.NullTranslationHelper) - require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) - - assert.Equal(t, "run_workflow", toolDef.Tool.Name) - assert.NotEmpty(t, toolDef.Tool.Description) - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "owner") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "repo") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "workflow_id") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "ref") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "inputs") - assert.ElementsMatch(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "workflow_id", "ref"}) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - }{ - { - name: "successful workflow run", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNoContent) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "workflow_id": "12345", - "ref": "main", - }, - expectError: false, - }, - { - name: "missing required parameter workflow_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "ref": "main", - }, - expectError: true, - expectedErrMsg: "missing required parameter: workflow_id", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup client with mock - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := toolDef.Handler(deps) - - // Create call request - request := createMCPRequest(tc.requestArgs) - - // Call handler - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - // Parse the result and get the text content if no error - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Equal(t, tc.expectedErrMsg, textContent.Text) - return - } - - // Unmarshal and verify the result - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - assert.Equal(t, "Workflow run has been queued", response["message"]) - assert.Contains(t, response, "workflow_type") - }) - } -} - -func Test_RunWorkflow_WithFilename(t *testing.T) { - // Test the unified RunWorkflow function with filenames - toolDef := RunWorkflow(translations.NullTranslationHelper) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - }{ - { - name: "successful workflow run by filename", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNoContent) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "workflow_id": "ci.yml", - "ref": "main", - }, - expectError: false, - }, - { - name: "successful workflow run by numeric ID as string", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNoContent) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "workflow_id": "12345", - "ref": "main", - }, - expectError: false, - }, - { - name: "missing required parameter workflow_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "ref": "main", - }, - expectError: true, - expectedErrMsg: "missing required parameter: workflow_id", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup client with mock - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := toolDef.Handler(deps) - - // Create call request - request := createMCPRequest(tc.requestArgs) - - // Call handler - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - // Parse the result and get the text content if no error - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Equal(t, tc.expectedErrMsg, textContent.Text) - return - } - - // Unmarshal and verify the result - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - assert.Equal(t, "Workflow run has been queued", response["message"]) - assert.Contains(t, response, "workflow_type") - }) - } -} - -func Test_CancelWorkflowRun(t *testing.T) { - // Verify tool definition once - toolDef := CancelWorkflowRun(translations.NullTranslationHelper) - require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) - - assert.Equal(t, "cancel_workflow_run", toolDef.Tool.Name) - assert.NotEmpty(t, toolDef.Tool.Description) - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "owner") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "repo") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "run_id") - assert.ElementsMatch(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "run_id"}) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - }{ - { - name: "successful workflow run cancellation", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - "POST /repos/owner/repo/actions/runs/12345/cancel": http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusAccepted) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "run_id": float64(12345), - }, - expectError: false, - }, - { - name: "conflict when cancelling a workflow run", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - "POST /repos/owner/repo/actions/runs/12345/cancel": http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusConflict) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "run_id": float64(12345), - }, - expectError: true, - expectedErrMsg: "failed to cancel workflow run", - }, - { - name: "missing required parameter run_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - }, - expectError: true, - expectedErrMsg: "missing required parameter: run_id", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup client with mock - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := toolDef.Handler(deps) - - // Create call request - request := createMCPRequest(tc.requestArgs) - - // Call handler - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - // Parse the result and get the text content - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Contains(t, textContent.Text, tc.expectedErrMsg) - return - } - - // Unmarshal and verify the result - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - assert.Equal(t, "Workflow run has been cancelled", response["message"]) - assert.Equal(t, float64(12345), response["run_id"]) - }) - } -} - -func Test_ListWorkflowRunArtifacts(t *testing.T) { - // Verify tool definition once - toolDef := ListWorkflowRunArtifacts(translations.NullTranslationHelper) - require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) - - assert.Equal(t, "list_workflow_run_artifacts", toolDef.Tool.Name) - assert.NotEmpty(t, toolDef.Tool.Description) - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "owner") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "repo") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "run_id") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "perPage") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "page") - assert.ElementsMatch(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "run_id"}) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - }{ - { - name: "successful artifacts listing", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsRunsArtifactsByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - artifacts := &github.ArtifactList{ - TotalCount: github.Ptr(int64(2)), - Artifacts: []*github.Artifact{ - { - ID: github.Ptr(int64(1)), - NodeID: github.Ptr("A_1"), - Name: github.Ptr("build-artifacts"), - SizeInBytes: github.Ptr(int64(1024)), - URL: github.Ptr("https://api.github.com/repos/owner/repo/actions/artifacts/1"), - ArchiveDownloadURL: github.Ptr("https://api.github.com/repos/owner/repo/actions/artifacts/1/zip"), - Expired: github.Ptr(false), - CreatedAt: &github.Timestamp{}, - UpdatedAt: &github.Timestamp{}, - ExpiresAt: &github.Timestamp{}, - WorkflowRun: &github.ArtifactWorkflowRun{ - ID: github.Ptr(int64(12345)), - RepositoryID: github.Ptr(int64(1)), - HeadRepositoryID: github.Ptr(int64(1)), - HeadBranch: github.Ptr("main"), - HeadSHA: github.Ptr("abc123"), - }, - }, - { - ID: github.Ptr(int64(2)), - NodeID: github.Ptr("A_2"), - Name: github.Ptr("test-results"), - SizeInBytes: github.Ptr(int64(512)), - URL: github.Ptr("https://api.github.com/repos/owner/repo/actions/artifacts/2"), - ArchiveDownloadURL: github.Ptr("https://api.github.com/repos/owner/repo/actions/artifacts/2/zip"), - Expired: github.Ptr(false), - CreatedAt: &github.Timestamp{}, - UpdatedAt: &github.Timestamp{}, - ExpiresAt: &github.Timestamp{}, - WorkflowRun: &github.ArtifactWorkflowRun{ - ID: github.Ptr(int64(12345)), - RepositoryID: github.Ptr(int64(1)), - HeadRepositoryID: github.Ptr(int64(1)), - HeadBranch: github.Ptr("main"), - HeadSHA: github.Ptr("abc123"), - }, - }, - }, - } - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(artifacts) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "run_id": float64(12345), - }, - expectError: false, - }, - { - name: "missing required parameter run_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - }, - expectError: true, - expectedErrMsg: "missing required parameter: run_id", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup client with mock - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := toolDef.Handler(deps) - - // Create call request - request := createMCPRequest(tc.requestArgs) - - // Call handler - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - // Parse the result and get the text content if no error - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Equal(t, tc.expectedErrMsg, textContent.Text) - return - } - - // Unmarshal and verify the result - var response github.ArtifactList - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - assert.NotNil(t, response.TotalCount) - assert.Greater(t, *response.TotalCount, int64(0)) - assert.NotEmpty(t, response.Artifacts) - }) - } -} - -func Test_DownloadWorkflowRunArtifact(t *testing.T) { - // Verify tool definition once - toolDef := DownloadWorkflowRunArtifact(translations.NullTranslationHelper) - require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) - - assert.Equal(t, "download_workflow_run_artifact", toolDef.Tool.Name) - assert.NotEmpty(t, toolDef.Tool.Description) - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "owner") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "repo") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "artifact_id") - assert.ElementsMatch(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "artifact_id"}) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - }{ - { - name: "successful artifact download URL", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - "GET /repos/owner/repo/actions/artifacts/123/zip": http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - // GitHub returns a 302 redirect to the download URL - w.Header().Set("Location", "https://api.github.com/repos/owner/repo/actions/artifacts/123/download") - w.WriteHeader(http.StatusFound) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "artifact_id": float64(123), - }, - expectError: false, - }, - { - name: "missing required parameter artifact_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - }, - expectError: true, - expectedErrMsg: "missing required parameter: artifact_id", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup client with mock - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := toolDef.Handler(deps) - - // Create call request - request := createMCPRequest(tc.requestArgs) - - // Call handler - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - // Parse the result and get the text content if no error - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Equal(t, tc.expectedErrMsg, textContent.Text) - return - } - - // Unmarshal and verify the result - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - assert.Contains(t, response, "download_url") - assert.Contains(t, response, "message") - assert.Equal(t, "Artifact is available for download", response["message"]) - assert.Equal(t, float64(123), response["artifact_id"]) - }) - } -} - -func Test_DeleteWorkflowRunLogs(t *testing.T) { - // Verify tool definition once - toolDef := DeleteWorkflowRunLogs(translations.NullTranslationHelper) - require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) - - assert.Equal(t, "delete_workflow_run_logs", toolDef.Tool.Name) - assert.NotEmpty(t, toolDef.Tool.Description) - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "owner") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "repo") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "run_id") - assert.ElementsMatch(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "run_id"}) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - }{ - { - name: "successful logs deletion", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - DeleteReposActionsRunsLogsByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNoContent) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "run_id": float64(12345), - }, - expectError: false, - }, - { - name: "missing required parameter run_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - }, - expectError: true, - expectedErrMsg: "missing required parameter: run_id", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup client with mock - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := toolDef.Handler(deps) - - // Create call request - request := createMCPRequest(tc.requestArgs) - - // Call handler - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - // Parse the result and get the text content if no error - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Equal(t, tc.expectedErrMsg, textContent.Text) - return - } - - // Unmarshal and verify the result - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - assert.Equal(t, "Workflow run logs have been deleted", response["message"]) - assert.Equal(t, float64(12345), response["run_id"]) - }) - } -} - -func Test_GetWorkflowRunUsage(t *testing.T) { - // Verify tool definition once - toolDef := GetWorkflowRunUsage(translations.NullTranslationHelper) - require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) - - assert.Equal(t, "get_workflow_run_usage", toolDef.Tool.Name) - assert.NotEmpty(t, toolDef.Tool.Description) - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "owner") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "repo") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "run_id") - assert.ElementsMatch(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "run_id"}) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - }{ - { - name: "successful workflow run usage", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsRunsTimingByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - usage := &github.WorkflowRunUsage{ - Billable: &github.WorkflowRunBillMap{ - "UBUNTU": &github.WorkflowRunBill{ - TotalMS: github.Ptr(int64(120000)), - Jobs: github.Ptr(2), - JobRuns: []*github.WorkflowRunJobRun{ - { - JobID: github.Ptr(1), - DurationMS: github.Ptr(int64(60000)), - }, - { - JobID: github.Ptr(2), - DurationMS: github.Ptr(int64(60000)), - }, - }, - }, - }, - RunDurationMS: github.Ptr(int64(120000)), - } - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(usage) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "run_id": float64(12345), - }, - expectError: false, - }, - { - name: "missing required parameter run_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - }, - expectError: true, - expectedErrMsg: "missing required parameter: run_id", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup client with mock - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := toolDef.Handler(deps) - - // Create call request - request := createMCPRequest(tc.requestArgs) - - // Call handler - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - // Parse the result and get the text content if no error - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Equal(t, tc.expectedErrMsg, textContent.Text) - return - } - - // Unmarshal and verify the result - var response github.WorkflowRunUsage - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - assert.NotNil(t, response.RunDurationMS) - assert.NotNil(t, response.Billable) - }) - } -} - -func Test_GetJobLogs(t *testing.T) { - // Verify tool definition once - toolDef := GetJobLogs(translations.NullTranslationHelper) - require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) - - assert.Equal(t, "get_job_logs", toolDef.Tool.Name) - assert.NotEmpty(t, toolDef.Tool.Description) - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "owner") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "repo") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "job_id") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "run_id") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "failed_only") - assert.Contains(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Properties, "return_content") - assert.ElementsMatch(t, toolDef.Tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo"}) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - checkResponse func(t *testing.T, response map[string]any) - }{ - { - name: "successful single job logs with URL", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsJobsLogsByOwnerByRepoByJobID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Location", "https://github.com/logs/job/123") - w.WriteHeader(http.StatusFound) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "job_id": float64(123), - }, - expectError: false, - checkResponse: func(t *testing.T, response map[string]any) { - assert.Equal(t, float64(123), response["job_id"]) - assert.Contains(t, response, "logs_url") - assert.Equal(t, "Job logs are available for download", response["message"]) - assert.Contains(t, response, "note") - }, - }, - { - name: "successful failed jobs logs", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsRunsJobsByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - jobs := &github.Jobs{ - TotalCount: github.Ptr(3), - Jobs: []*github.WorkflowJob{ - { - ID: github.Ptr(int64(1)), - Name: github.Ptr("test-job-1"), - Conclusion: github.Ptr("success"), - }, - { - ID: github.Ptr(int64(2)), - Name: github.Ptr("test-job-2"), - Conclusion: github.Ptr("failure"), - }, - { - ID: github.Ptr(int64(3)), - Name: github.Ptr("test-job-3"), - Conclusion: github.Ptr("failure"), - }, - }, - } - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(jobs) - }), - GetReposActionsJobsLogsByOwnerByRepoByJobID: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Location", "https://github.com/logs/job/"+r.URL.Path[len(r.URL.Path)-1:]) - w.WriteHeader(http.StatusFound) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "run_id": float64(456), - "failed_only": true, - }, - expectError: false, - checkResponse: func(t *testing.T, response map[string]any) { - assert.Equal(t, float64(456), response["run_id"]) - assert.Equal(t, float64(3), response["total_jobs"]) - assert.Equal(t, float64(2), response["failed_jobs"]) - assert.Contains(t, response, "logs") - assert.Equal(t, "Retrieved logs for 2 failed jobs", response["message"]) - - logs, ok := response["logs"].([]interface{}) - assert.True(t, ok) - assert.Len(t, logs, 2) - }, - }, - { - name: "no failed jobs found", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsRunsJobsByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - jobs := &github.Jobs{ - TotalCount: github.Ptr(2), - Jobs: []*github.WorkflowJob{ - { - ID: github.Ptr(int64(1)), - Name: github.Ptr("test-job-1"), - Conclusion: github.Ptr("success"), - }, - { - ID: github.Ptr(int64(2)), - Name: github.Ptr("test-job-2"), - Conclusion: github.Ptr("success"), - }, - }, - } - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(jobs) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "run_id": float64(456), - "failed_only": true, - }, - expectError: false, - checkResponse: func(t *testing.T, response map[string]any) { - assert.Equal(t, "No failed jobs found in this workflow run", response["message"]) - assert.Equal(t, float64(456), response["run_id"]) - assert.Equal(t, float64(2), response["total_jobs"]) - assert.Equal(t, float64(0), response["failed_jobs"]) - }, - }, - { - name: "missing job_id when not using failed_only", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - }, - expectError: true, - expectedErrMsg: "job_id is required when failed_only is false", - }, - { - name: "missing run_id when using failed_only", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "failed_only": true, - }, - expectError: true, - expectedErrMsg: "run_id is required when failed_only is true", - }, - { - name: "missing required parameter owner", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "repo": "repo", - "job_id": float64(123), - }, - expectError: true, - expectedErrMsg: "missing required parameter: owner", - }, - { - name: "missing required parameter repo", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "job_id": float64(123), - }, - expectError: true, - expectedErrMsg: "missing required parameter: repo", - }, - { - name: "API error when getting single job logs", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsJobsLogsByOwnerByRepoByJobID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(map[string]string{ - "message": "Not Found", - }) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "job_id": float64(999), - }, - expectError: true, - }, - { - name: "API error when listing workflow jobs for failed_only", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsRunsJobsByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(map[string]string{ - "message": "Not Found", - }) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "run_id": float64(999), - "failed_only": true, - }, - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup client with mock - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - ContentWindowSize: 5000, - } - handler := toolDef.Handler(deps) - - // Create call request - request := createMCPRequest(tc.requestArgs) - - // Call handler - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - // Parse the result and get the text content - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Equal(t, tc.expectedErrMsg, textContent.Text) - return - } - - if tc.expectError { - // For API errors, just verify we got an error - assert.True(t, result.IsError) - return - } - - // Unmarshal and verify the result - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - - if tc.checkResponse != nil { - tc.checkResponse(t, response) - } - }) - } -} - -func Test_GetJobLogs_WithContentReturn(t *testing.T) { - // Test the return_content functionality with a mock HTTP server - logContent := "2023-01-01T10:00:00.000Z Starting job...\n2023-01-01T10:00:01.000Z Running tests...\n2023-01-01T10:00:02.000Z Job completed successfully" - - // Create a test server to serve log content - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(logContent)) - })) - defer testServer.Close() - - mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsJobsLogsByOwnerByRepoByJobID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Location", testServer.URL) - w.WriteHeader(http.StatusFound) - }), - }) - - client := github.NewClient(mockedClient) - toolDef := GetJobLogs(translations.NullTranslationHelper) - deps := BaseDeps{ - Client: client, - ContentWindowSize: 5000, - } - handler := toolDef.Handler(deps) - - request := createMCPRequest(map[string]any{ - "owner": "owner", - "repo": "repo", - "job_id": float64(123), - "return_content": true, - }) - - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - require.NoError(t, err) - require.False(t, result.IsError) - - textContent := getTextResult(t, result) - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - - assert.Equal(t, float64(123), response["job_id"]) - assert.Equal(t, logContent, response["logs_content"]) - assert.Equal(t, "Job logs content retrieved successfully", response["message"]) - assert.NotContains(t, response, "logs_url") // Should not have URL when returning content -} - -func Test_GetJobLogs_WithContentReturnAndTailLines(t *testing.T) { - // Test the return_content functionality with a mock HTTP server - logContent := "2023-01-01T10:00:00.000Z Starting job...\n2023-01-01T10:00:01.000Z Running tests...\n2023-01-01T10:00:02.000Z Job completed successfully" - expectedLogContent := "2023-01-01T10:00:02.000Z Job completed successfully" - - // Create a test server to serve log content - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(logContent)) - })) - defer testServer.Close() - - mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsJobsLogsByOwnerByRepoByJobID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Location", testServer.URL) - w.WriteHeader(http.StatusFound) - }), - }) - - client := github.NewClient(mockedClient) - toolDef := GetJobLogs(translations.NullTranslationHelper) - deps := BaseDeps{ - Client: client, - ContentWindowSize: 5000, - } - handler := toolDef.Handler(deps) - - request := createMCPRequest(map[string]any{ - "owner": "owner", - "repo": "repo", - "job_id": float64(123), - "return_content": true, - "tail_lines": float64(1), // Requesting last 1 line - }) - - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - require.NoError(t, err) - require.False(t, result.IsError) - - textContent := getTextResult(t, result) - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - - assert.Equal(t, float64(123), response["job_id"]) - assert.Equal(t, float64(3), response["original_length"]) - assert.Equal(t, expectedLogContent, response["logs_content"]) - assert.Equal(t, "Job logs content retrieved successfully", response["message"]) - assert.NotContains(t, response, "logs_url") // Should not have URL when returning content -} - -func Test_GetJobLogs_WithContentReturnAndLargeTailLines(t *testing.T) { - logContent := "Line 1\nLine 2\nLine 3" - expectedLogContent := "Line 1\nLine 2\nLine 3" - - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(logContent)) - })) - defer testServer.Close() - - mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsJobsLogsByOwnerByRepoByJobID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Location", testServer.URL) - w.WriteHeader(http.StatusFound) - }), - }) - - client := github.NewClient(mockedClient) - toolDef := GetJobLogs(translations.NullTranslationHelper) - deps := BaseDeps{ - Client: client, - ContentWindowSize: 5000, - } - handler := toolDef.Handler(deps) - - request := createMCPRequest(map[string]any{ - "owner": "owner", - "repo": "repo", - "job_id": float64(123), - "return_content": true, - "tail_lines": float64(100), - }) - - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - require.NoError(t, err) - require.False(t, result.IsError) - - textContent := getTextResult(t, result) - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - - assert.Equal(t, float64(123), response["job_id"]) - assert.Equal(t, float64(3), response["original_length"]) - assert.Equal(t, expectedLogContent, response["logs_content"]) - assert.Equal(t, "Job logs content retrieved successfully", response["message"]) - assert.NotContains(t, response, "logs_url") -} - -func Test_MemoryUsage_SlidingWindow_vs_NoWindow(t *testing.T) { - if testing.Short() { - t.Skip("Skipping memory profiling test in short mode") - } - - const logLines = 100000 - const bufferSize = 5000 - largeLogContent := strings.Repeat("log line with some content\n", logLines-1) + "final log line" - - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(largeLogContent)) - })) - defer testServer.Close() - - os.Setenv("GITHUB_MCP_PROFILING_ENABLED", "true") - defer os.Unsetenv("GITHUB_MCP_PROFILING_ENABLED") - - profiler.InitFromEnv(nil) - ctx := context.Background() - - debug.SetGCPercent(-1) - defer debug.SetGCPercent(100) - - for i := 0; i < 3; i++ { - runtime.GC() - } - - var baselineStats runtime.MemStats - runtime.ReadMemStats(&baselineStats) - - profile1, err1 := profiler.ProfileFuncWithMetrics(ctx, "sliding_window", func() (int, int64, error) { - resp1, err := http.Get(testServer.URL) - if err != nil { - return 0, 0, err - } - defer resp1.Body.Close() //nolint:bodyclose - content, totalLines, _, err := buffer.ProcessResponseAsRingBufferToEnd(resp1, bufferSize) //nolint:bodyclose - return totalLines, int64(len(content)), err - }) - require.NoError(t, err1) - - for i := 0; i < 3; i++ { - runtime.GC() - } - - profile2, err2 := profiler.ProfileFuncWithMetrics(ctx, "no_window", func() (int, int64, error) { - resp2, err := http.Get(testServer.URL) - if err != nil { - return 0, 0, err - } - defer resp2.Body.Close() //nolint:bodyclose - - allContent, err := io.ReadAll(resp2.Body) - if err != nil { - return 0, 0, err - } - - allLines := strings.Split(string(allContent), "\n") - var nonEmptyLines []string - for _, line := range allLines { - if line != "" { - nonEmptyLines = append(nonEmptyLines, line) - } - } - totalLines := len(nonEmptyLines) - - var resultLines []string - if totalLines > bufferSize { - resultLines = nonEmptyLines[totalLines-bufferSize:] - } else { - resultLines = nonEmptyLines - } - - result := strings.Join(resultLines, "\n") - return totalLines, int64(len(result)), nil - }) - require.NoError(t, err2) - - assert.Greater(t, profile2.MemoryDelta, profile1.MemoryDelta, - "Sliding window should use less memory than reading all into memory") - - assert.Equal(t, profile1.LinesCount, profile2.LinesCount, - "Both approaches should count the same number of input lines") - assert.InDelta(t, profile1.BytesCount, profile2.BytesCount, 100, - "Both approaches should produce similar output sizes (within 100 bytes)") - - memoryReduction := float64(profile2.MemoryDelta-profile1.MemoryDelta) / float64(profile2.MemoryDelta) * 100 - t.Logf("Memory reduction: %.1f%% (%.2f MB vs %.2f MB)", - memoryReduction, - float64(profile2.MemoryDelta)/1024/1024, - float64(profile1.MemoryDelta)/1024/1024) - - t.Logf("Baseline: %d bytes", baselineStats.Alloc) - t.Logf("Sliding window: %s", profile1.String()) - t.Logf("No window: %s", profile2.String()) -} - -func Test_ListWorkflowRuns(t *testing.T) { - // Verify tool definition once - toolDef := ListWorkflowRuns(translations.NullTranslationHelper) - require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) - - assert.Equal(t, "list_workflow_runs", toolDef.Tool.Name) - assert.NotEmpty(t, toolDef.Tool.Description) - inputSchema := toolDef.Tool.InputSchema.(*jsonschema.Schema) - assert.Contains(t, inputSchema.Properties, "owner") - assert.Contains(t, inputSchema.Properties, "repo") - assert.Contains(t, inputSchema.Properties, "workflow_id") - assert.ElementsMatch(t, inputSchema.Required, []string{"owner", "repo", "workflow_id"}) -} - -func Test_GetWorkflowRun(t *testing.T) { - // Verify tool definition once - toolDef := GetWorkflowRun(translations.NullTranslationHelper) - require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) - - assert.Equal(t, "get_workflow_run", toolDef.Tool.Name) - assert.NotEmpty(t, toolDef.Tool.Description) - inputSchema := toolDef.Tool.InputSchema.(*jsonschema.Schema) - assert.Contains(t, inputSchema.Properties, "owner") - assert.Contains(t, inputSchema.Properties, "repo") - assert.Contains(t, inputSchema.Properties, "run_id") - assert.ElementsMatch(t, inputSchema.Required, []string{"owner", "repo", "run_id"}) -} - -func Test_GetWorkflowRunLogs(t *testing.T) { - // Verify tool definition once - toolDef := GetWorkflowRunLogs(translations.NullTranslationHelper) - require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) - - assert.Equal(t, "get_workflow_run_logs", toolDef.Tool.Name) - assert.NotEmpty(t, toolDef.Tool.Description) - inputSchema := toolDef.Tool.InputSchema.(*jsonschema.Schema) - assert.Contains(t, inputSchema.Properties, "owner") - assert.Contains(t, inputSchema.Properties, "repo") - assert.Contains(t, inputSchema.Properties, "run_id") - assert.ElementsMatch(t, inputSchema.Required, []string{"owner", "repo", "run_id"}) -} - -func Test_ListWorkflowJobs(t *testing.T) { - // Verify tool definition once - toolDef := ListWorkflowJobs(translations.NullTranslationHelper) - require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) - - assert.Equal(t, "list_workflow_jobs", toolDef.Tool.Name) - assert.NotEmpty(t, toolDef.Tool.Description) - inputSchema := toolDef.Tool.InputSchema.(*jsonschema.Schema) - assert.Contains(t, inputSchema.Properties, "owner") - assert.Contains(t, inputSchema.Properties, "repo") - assert.Contains(t, inputSchema.Properties, "run_id") - assert.ElementsMatch(t, inputSchema.Required, []string{"owner", "repo", "run_id"}) -} - -func Test_RerunWorkflowRun(t *testing.T) { - // Verify tool definition once - toolDef := RerunWorkflowRun(translations.NullTranslationHelper) - require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) - - assert.Equal(t, "rerun_workflow_run", toolDef.Tool.Name) - assert.NotEmpty(t, toolDef.Tool.Description) - inputSchema := toolDef.Tool.InputSchema.(*jsonschema.Schema) - assert.Contains(t, inputSchema.Properties, "owner") - assert.Contains(t, inputSchema.Properties, "repo") - assert.Contains(t, inputSchema.Properties, "run_id") - assert.ElementsMatch(t, inputSchema.Required, []string{"owner", "repo", "run_id"}) -} - -func Test_RerunFailedJobs(t *testing.T) { - // Verify tool definition once - toolDef := RerunFailedJobs(translations.NullTranslationHelper) - require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) - - assert.Equal(t, "rerun_failed_jobs", toolDef.Tool.Name) - assert.NotEmpty(t, toolDef.Tool.Description) - inputSchema := toolDef.Tool.InputSchema.(*jsonschema.Schema) - assert.Contains(t, inputSchema.Properties, "owner") - assert.Contains(t, inputSchema.Properties, "repo") - assert.Contains(t, inputSchema.Properties, "run_id") - assert.ElementsMatch(t, inputSchema.Required, []string{"owner", "repo", "run_id"}) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - }{ - { - name: "successful rerun of failed jobs", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - PostReposActionsRunsRerunFailedJobsByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusCreated) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "run_id": float64(12345), - }, - expectError: false, - }, - { - name: "missing required parameter run_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - }, - expectError: true, - expectedErrMsg: "missing required parameter: run_id", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := toolDef.Handler(deps) - - request := createMCPRequest(tc.requestArgs) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Equal(t, tc.expectedErrMsg, textContent.Text) - return - } - - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - assert.Equal(t, "Failed jobs have been queued for re-run", response["message"]) - assert.Equal(t, float64(12345), response["run_id"]) - }) - } -} - -func Test_RerunWorkflowRun_Behavioral(t *testing.T) { - toolDef := RerunWorkflowRun(translations.NullTranslationHelper) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - }{ - { - name: "successful rerun of workflow run", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - PostReposActionsRunsRerunByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusCreated) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "run_id": float64(12345), - }, - expectError: false, - }, - { - name: "missing required parameter run_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - }, - expectError: true, - expectedErrMsg: "missing required parameter: run_id", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := toolDef.Handler(deps) - - request := createMCPRequest(tc.requestArgs) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Equal(t, tc.expectedErrMsg, textContent.Text) - return - } - - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - assert.Equal(t, "Workflow run has been queued for re-run", response["message"]) - assert.Equal(t, float64(12345), response["run_id"]) - }) - } -} - -func Test_ListWorkflowRuns_Behavioral(t *testing.T) { - toolDef := ListWorkflowRuns(translations.NullTranslationHelper) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - }{ - { - name: "successful workflow runs listing", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsWorkflowsRunsByOwnerByRepoByWorkflowID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - runs := &github.WorkflowRuns{ - TotalCount: github.Ptr(2), - WorkflowRuns: []*github.WorkflowRun{ - { - ID: github.Ptr(int64(123)), - Name: github.Ptr("CI"), - Status: github.Ptr("completed"), - Conclusion: github.Ptr("success"), - }, - { - ID: github.Ptr(int64(456)), - Name: github.Ptr("CI"), - Status: github.Ptr("completed"), - Conclusion: github.Ptr("failure"), - }, - }, - } - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(runs) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "workflow_id": "ci.yml", - }, - expectError: false, - }, - { - name: "missing required parameter workflow_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - }, - expectError: true, - expectedErrMsg: "missing required parameter: workflow_id", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := toolDef.Handler(deps) - - request := createMCPRequest(tc.requestArgs) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Equal(t, tc.expectedErrMsg, textContent.Text) - return - } - - var response github.WorkflowRuns - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - assert.NotNil(t, response.TotalCount) - assert.Greater(t, *response.TotalCount, 0) - }) - } -} - -func Test_GetWorkflowRun_Behavioral(t *testing.T) { - toolDef := GetWorkflowRun(translations.NullTranslationHelper) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - }{ - { - name: "successful get workflow run", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsRunsByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - run := &github.WorkflowRun{ - ID: github.Ptr(int64(12345)), - Name: github.Ptr("CI"), - Status: github.Ptr("completed"), - Conclusion: github.Ptr("success"), - } - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(run) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "run_id": float64(12345), - }, - expectError: false, - }, - { - name: "missing required parameter run_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - }, - expectError: true, - expectedErrMsg: "missing required parameter: run_id", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := toolDef.Handler(deps) - - request := createMCPRequest(tc.requestArgs) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Equal(t, tc.expectedErrMsg, textContent.Text) - return - } - - var response github.WorkflowRun - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - assert.NotNil(t, response.ID) - assert.Equal(t, int64(12345), *response.ID) - }) - } -} - -func Test_GetWorkflowRunLogs_Behavioral(t *testing.T) { - toolDef := GetWorkflowRunLogs(translations.NullTranslationHelper) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - }{ - { - name: "successful get workflow run logs", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsRunsLogsByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Location", "https://github.com/logs/run/12345") - w.WriteHeader(http.StatusFound) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "run_id": float64(12345), - }, - expectError: false, - }, - { - name: "missing required parameter run_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - }, - expectError: true, - expectedErrMsg: "missing required parameter: run_id", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := toolDef.Handler(deps) - - request := createMCPRequest(tc.requestArgs) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Equal(t, tc.expectedErrMsg, textContent.Text) - return - } - - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - assert.Contains(t, response, "logs_url") - assert.Equal(t, "Workflow run logs are available for download", response["message"]) - }) - } -} - -func Test_ListWorkflowJobs_Behavioral(t *testing.T) { - toolDef := ListWorkflowJobs(translations.NullTranslationHelper) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - }{ - { - name: "successful list workflow jobs", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetReposActionsRunsJobsByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - jobs := &github.Jobs{ - TotalCount: github.Ptr(2), - Jobs: []*github.WorkflowJob{ - { - ID: github.Ptr(int64(1)), - Name: github.Ptr("build"), - Status: github.Ptr("completed"), - Conclusion: github.Ptr("success"), - }, - { - ID: github.Ptr(int64(2)), - Name: github.Ptr("test"), - Status: github.Ptr("completed"), - Conclusion: github.Ptr("failure"), - }, - }, - } - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(jobs) - }), - }), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "run_id": float64(12345), - }, - expectError: false, - }, - { - name: "missing required parameter run_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - }, - expectError: true, - expectedErrMsg: "missing required parameter: run_id", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := github.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := toolDef.Handler(deps) - - request := createMCPRequest(tc.requestArgs) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - require.Equal(t, tc.expectError, result.IsError) - - textContent := getTextResult(t, result) - - if tc.expectedErrMsg != "" { - assert.Equal(t, tc.expectedErrMsg, textContent.Text) - return - } - - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - assert.Contains(t, response, "jobs") - }) - } -} - // Tests for consolidated actions tools func Test_ActionsList(t *testing.T) { diff --git a/pkg/github/projects.go b/pkg/github/projects.go index 4fed6364f..be99e6dd3 100644 --- a/pkg/github/projects.go +++ b/pkg/github/projects.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "net/http" - "strings" ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/inventory" @@ -27,1003 +26,18 @@ const ( MaxProjectsPerPage = 50 ) -// FeatureFlagHoldbackConsolidatedProjects is the feature flag that, when enabled, reverts to -// individual project tools instead of the consolidated project tools. -const FeatureFlagHoldbackConsolidatedProjects = "mcp_holdback_consolidated_projects" - -// Method constants for consolidated project tools -const ( - projectsMethodListProjects = "list_projects" - projectsMethodListProjectFields = "list_project_fields" - projectsMethodListProjectItems = "list_project_items" - projectsMethodGetProject = "get_project" - projectsMethodGetProjectField = "get_project_field" - projectsMethodGetProjectItem = "get_project_item" - projectsMethodAddProjectItem = "add_project_item" - projectsMethodUpdateProjectItem = "update_project_item" - projectsMethodDeleteProjectItem = "delete_project_item" -) - -func ListProjects(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataProjects, - mcp.Tool{ - Name: "list_projects", - Description: t("TOOL_LIST_PROJECTS_DESCRIPTION", `List Projects for a user or organization`), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_LIST_PROJECTS_USER_TITLE", "List projects"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner_type": { - Type: "string", - Description: "Owner type", - Enum: []any{"user", "org"}, - }, - "owner": { - Type: "string", - Description: "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - }, - "query": { - Type: "string", - Description: `Filter projects by title text and open/closed state; permitted qualifiers: is:open, is:closed; examples: "roadmap is:open", "is:open feature planning".`, - }, - "per_page": { - Type: "number", - Description: fmt.Sprintf("Results per page (max %d)", MaxProjectsPerPage), - }, - "after": { - Type: "string", - Description: "Forward pagination cursor from previous pageInfo.nextCursor.", - }, - "before": { - Type: "string", - Description: "Backward pagination cursor from previous pageInfo.prevCursor (rare).", - }, - }, - Required: []string{"owner_type", "owner"}, - }, - }, - []scopes.Scope{scopes.ReadProject}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - queryStr, err := OptionalParam[string](args, "query") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - pagination, err := extractPaginationOptionsFromArgs(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - var resp *github.Response - var projects []*github.ProjectV2 - var queryPtr *string - - if queryStr != "" { - queryPtr = &queryStr - } - - minimalProjects := []MinimalProject{} - opts := &github.ListProjectsOptions{ - ListProjectsPaginationOptions: pagination, - Query: queryPtr, - } - - if ownerType == "org" { - projects, resp, err = client.Projects.ListOrganizationProjects(ctx, owner, opts) - } else { - projects, resp, err = client.Projects.ListUserProjects(ctx, owner, opts) - } - - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to list projects", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - for _, project := range projects { - minimalProjects = append(minimalProjects, *convertToMinimalProject(project)) - } - - response := map[string]any{ - "projects": minimalProjects, - "pageInfo": buildPageInfo(resp), - } - - r, err := json.Marshal(response) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedProjects - return tool -} - -func GetProject(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataProjects, - mcp.Tool{ - Name: "get_project", - Description: t("TOOL_GET_PROJECT_DESCRIPTION", "Get Project for a user or org"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_PROJECT_USER_TITLE", "Get project"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "project_number": { - Type: "number", - Description: "The project's number", - }, - "owner_type": { - Type: "string", - Description: "Owner type", - Enum: []any{"user", "org"}, - }, - "owner": { - Type: "string", - Description: "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - }, - }, - Required: []string{"project_number", "owner_type", "owner"}, - }, - }, - []scopes.Scope{scopes.ReadProject}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - var resp *github.Response - var project *github.ProjectV2 - - if ownerType == "org" { - project, resp, err = client.Projects.GetOrganizationProject(ctx, owner, projectNumber) - } else { - project, resp, err = client.Projects.GetUserProject(ctx, owner, projectNumber) - } - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get project", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get project", resp, body), nil, nil - } - - minimalProject := convertToMinimalProject(project) - r, err := json.Marshal(minimalProject) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedProjects - return tool -} - -func ListProjectFields(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataProjects, - mcp.Tool{ - Name: "list_project_fields", - Description: t("TOOL_LIST_PROJECT_FIELDS_DESCRIPTION", "List Project fields for a user or org"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_LIST_PROJECT_FIELDS_USER_TITLE", "List project fields"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner_type": { - Type: "string", - Description: "Owner type", - Enum: []any{"user", "org"}, - }, - "owner": { - Type: "string", - Description: "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - }, - "project_number": { - Type: "number", - Description: "The project's number.", - }, - "per_page": { - Type: "number", - Description: fmt.Sprintf("Results per page (max %d)", MaxProjectsPerPage), - }, - "after": { - Type: "string", - Description: "Forward pagination cursor from previous pageInfo.nextCursor.", - }, - "before": { - Type: "string", - Description: "Backward pagination cursor from previous pageInfo.prevCursor (rare).", - }, - }, - Required: []string{"owner_type", "owner", "project_number"}, - }, - }, - []scopes.Scope{scopes.ReadProject}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - pagination, err := extractPaginationOptionsFromArgs(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - var resp *github.Response - var projectFields []*github.ProjectV2Field - - opts := &github.ListProjectsOptions{ - ListProjectsPaginationOptions: pagination, - } - - if ownerType == "org" { - projectFields, resp, err = client.Projects.ListOrganizationProjectFields(ctx, owner, projectNumber, opts) - } else { - projectFields, resp, err = client.Projects.ListUserProjectFields(ctx, owner, projectNumber, opts) - } - - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to list project fields", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - response := map[string]any{ - "fields": projectFields, - "pageInfo": buildPageInfo(resp), - } - - r, err := json.Marshal(response) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedProjects - return tool -} - -func GetProjectField(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataProjects, - mcp.Tool{ - Name: "get_project_field", - Description: t("TOOL_GET_PROJECT_FIELD_DESCRIPTION", "Get Project field for a user or org"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_PROJECT_FIELD_USER_TITLE", "Get project field"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner_type": { - Type: "string", - Description: "Owner type", - Enum: []any{"user", "org"}, - }, - "owner": { - Type: "string", - Description: "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - }, - "project_number": { - Type: "number", - Description: "The project's number.", - }, - "field_id": { - Type: "number", - Description: "The field's id.", - }, - }, - Required: []string{"owner_type", "owner", "project_number", "field_id"}, - }, - }, - []scopes.Scope{scopes.ReadProject}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - fieldID, err := RequiredBigInt(args, "field_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - var resp *github.Response - var projectField *github.ProjectV2Field - - if ownerType == "org" { - projectField, resp, err = client.Projects.GetOrganizationProjectField(ctx, owner, projectNumber, fieldID) - } else { - projectField, resp, err = client.Projects.GetUserProjectField(ctx, owner, projectNumber, fieldID) - } - - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get project field", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get project field", resp, body), nil, nil - } - r, err := json.Marshal(projectField) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedProjects - return tool -} - -func ListProjectItems(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataProjects, - mcp.Tool{ - Name: "list_project_items", - Description: t("TOOL_LIST_PROJECT_ITEMS_DESCRIPTION", `Search project items with advanced filtering`), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_LIST_PROJECT_ITEMS_USER_TITLE", "List project items"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner_type": { - Type: "string", - Description: "Owner type", - Enum: []any{"user", "org"}, - }, - "owner": { - Type: "string", - Description: "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - }, - "project_number": { - Type: "number", - Description: "The project's number.", - }, - "query": { - Type: "string", - Description: `Query string for advanced filtering of project items using GitHub's project filtering syntax.`, - }, - "per_page": { - Type: "number", - Description: fmt.Sprintf("Results per page (max %d)", MaxProjectsPerPage), - }, - "after": { - Type: "string", - Description: "Forward pagination cursor from previous pageInfo.nextCursor.", - }, - "before": { - Type: "string", - Description: "Backward pagination cursor from previous pageInfo.prevCursor (rare).", - }, - "fields": { - Type: "array", - Description: "Field IDs to include (e.g. [\"102589\", \"985201\"]). CRITICAL: Always provide to get field values. Without this, only titles returned.", - Items: &jsonschema.Schema{ - Type: "string", - }, - }, - }, - Required: []string{"owner_type", "owner", "project_number"}, - }, - }, - []scopes.Scope{scopes.ReadProject}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - queryStr, err := OptionalParam[string](args, "query") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - fields, err := OptionalBigIntArrayParam(args, "fields") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - pagination, err := extractPaginationOptionsFromArgs(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - var resp *github.Response - var projectItems []*github.ProjectV2Item - var queryPtr *string - - if queryStr != "" { - queryPtr = &queryStr - } - - opts := &github.ListProjectItemsOptions{ - Fields: fields, - ListProjectsOptions: github.ListProjectsOptions{ - ListProjectsPaginationOptions: pagination, - Query: queryPtr, - }, - } - - if ownerType == "org" { - projectItems, resp, err = client.Projects.ListOrganizationProjectItems(ctx, owner, projectNumber, opts) - } else { - projectItems, resp, err = client.Projects.ListUserProjectItems(ctx, owner, projectNumber, opts) - } - - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - ProjectListFailedError, - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - response := map[string]any{ - "items": projectItems, - "pageInfo": buildPageInfo(resp), - } - - r, err := json.Marshal(response) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedProjects - return tool -} - -func GetProjectItem(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataProjects, - mcp.Tool{ - Name: "get_project_item", - Description: t("TOOL_GET_PROJECT_ITEM_DESCRIPTION", "Get a specific Project item for a user or org"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_GET_PROJECT_ITEM_USER_TITLE", "Get project item"), - ReadOnlyHint: true, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner_type": { - Type: "string", - Description: "Owner type", - Enum: []any{"user", "org"}, - }, - "owner": { - Type: "string", - Description: "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - }, - "project_number": { - Type: "number", - Description: "The project's number.", - }, - "item_id": { - Type: "number", - Description: "The item's ID.", - }, - "fields": { - Type: "array", - Description: "Specific list of field IDs to include in the response (e.g. [\"102589\", \"985201\", \"169875\"]). If not provided, only the title field is included.", - Items: &jsonschema.Schema{ - Type: "string", - }, - }, - }, - Required: []string{"owner_type", "owner", "project_number", "item_id"}, - }, - }, - []scopes.Scope{scopes.ReadProject}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - itemID, err := RequiredBigInt(args, "item_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - fields, err := OptionalBigIntArrayParam(args, "fields") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - var resp *github.Response - var projectItem *github.ProjectV2Item - var opts *github.GetProjectItemOptions - - if len(fields) > 0 { - opts = &github.GetProjectItemOptions{ - Fields: fields, - } - } - - if ownerType == "org" { - projectItem, resp, err = client.Projects.GetOrganizationProjectItem(ctx, owner, projectNumber, itemID, opts) - } else { - projectItem, resp, err = client.Projects.GetUserProjectItem(ctx, owner, projectNumber, itemID, opts) - } - - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get project item", - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - r, err := json.Marshal(projectItem) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedProjects - return tool -} - -func AddProjectItem(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataProjects, - mcp.Tool{ - Name: "add_project_item", - Description: t("TOOL_ADD_PROJECT_ITEM_DESCRIPTION", "Add a specific Project item for a user or org"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_ADD_PROJECT_ITEM_USER_TITLE", "Add project item"), - ReadOnlyHint: false, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner_type": { - Type: "string", - Description: "Owner type", - Enum: []any{"user", "org"}, - }, - "owner": { - Type: "string", - Description: "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - }, - "project_number": { - Type: "number", - Description: "The project's number.", - }, - "item_type": { - Type: "string", - Description: "The item's type, either issue or pull_request.", - Enum: []any{"issue", "pull_request"}, - }, - "item_id": { - Type: "number", - Description: "The numeric ID of the issue or pull request to add to the project.", - }, - }, - Required: []string{"owner_type", "owner", "project_number", "item_type", "item_id"}, - }, - }, - []scopes.Scope{scopes.Project}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - itemID, err := RequiredBigInt(args, "item_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - itemType, err := RequiredParam[string](args, "item_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - if itemType != "issue" && itemType != "pull_request" { - return utils.NewToolResultError("item_type must be either 'issue' or 'pull_request'"), nil, nil - } - - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - newItem := &github.AddProjectItemOptions{ - ID: itemID, - Type: toNewProjectType(itemType), - } - - var resp *github.Response - var addedItem *github.ProjectV2Item - - if ownerType == "org" { - addedItem, resp, err = client.Projects.AddOrganizationProjectItem(ctx, owner, projectNumber, newItem) - } else { - addedItem, resp, err = client.Projects.AddUserProjectItem(ctx, owner, projectNumber, newItem) - } - - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - ProjectAddFailedError, - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusCreated { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, ProjectAddFailedError, resp, body), nil, nil - } - r, err := json.Marshal(addedItem) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedProjects - return tool -} - -func UpdateProjectItem(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataProjects, - mcp.Tool{ - Name: "update_project_item", - Description: t("TOOL_UPDATE_PROJECT_ITEM_DESCRIPTION", "Update a specific Project item for a user or org"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_UPDATE_PROJECT_ITEM_USER_TITLE", "Update project item"), - ReadOnlyHint: false, - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner_type": { - Type: "string", - Description: "Owner type", - Enum: []any{"user", "org"}, - }, - "owner": { - Type: "string", - Description: "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - }, - "project_number": { - Type: "number", - Description: "The project's number.", - }, - "item_id": { - Type: "number", - Description: "The unique identifier of the project item. This is not the issue or pull request ID.", - }, - "updated_field": { - Type: "object", - Description: "Object consisting of the ID of the project field to update and the new value for the field. To clear the field, set value to null. Example: {\"id\": 123456, \"value\": \"New Value\"}", - }, - }, - Required: []string{"owner_type", "owner", "project_number", "item_id", "updated_field"}, - }, - }, - []scopes.Scope{scopes.Project}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - itemID, err := RequiredBigInt(args, "item_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - rawUpdatedField, exists := args["updated_field"] - if !exists { - return utils.NewToolResultError("missing required parameter: updated_field"), nil, nil - } - - fieldValue, ok := rawUpdatedField.(map[string]any) - if !ok || fieldValue == nil { - return utils.NewToolResultError("field_value must be an object"), nil, nil - } - - updatePayload, err := buildUpdateProjectItem(fieldValue) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - var resp *github.Response - var updatedItem *github.ProjectV2Item - - if ownerType == "org" { - updatedItem, resp, err = client.Projects.UpdateOrganizationProjectItem(ctx, owner, projectNumber, itemID, updatePayload) - } else { - updatedItem, resp, err = client.Projects.UpdateUserProjectItem(ctx, owner, projectNumber, itemID, updatePayload) - } - - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - ProjectUpdateFailedError, - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, ProjectUpdateFailedError, resp, body), nil, nil - } - r, err := json.Marshal(updatedItem) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return utils.NewToolResultText(string(r)), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedProjects - return tool -} - -func DeleteProjectItem(t translations.TranslationHelperFunc) inventory.ServerTool { - tool := NewTool( - ToolsetMetadataProjects, - mcp.Tool{ - Name: "delete_project_item", - Description: t("TOOL_DELETE_PROJECT_ITEM_DESCRIPTION", "Delete a specific Project item for a user or org"), - Annotations: &mcp.ToolAnnotations{ - Title: t("TOOL_DELETE_PROJECT_ITEM_USER_TITLE", "Delete project item"), - ReadOnlyHint: false, - DestructiveHint: jsonschema.Ptr(true), - }, - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "owner_type": { - Type: "string", - Description: "Owner type", - Enum: []any{"user", "org"}, - }, - "owner": { - Type: "string", - Description: "If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive.", - }, - "project_number": { - Type: "number", - Description: "The project's number.", - }, - "item_id": { - Type: "number", - Description: "The internal project item ID to delete from the project (not the issue or pull request ID).", - }, - }, - Required: []string{"owner_type", "owner", "project_number", "item_id"}, - }, - }, - []scopes.Scope{scopes.Project}, - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - ownerType, err := RequiredParam[string](args, "owner_type") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - projectNumber, err := RequiredInt(args, "project_number") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - itemID, err := RequiredBigInt(args, "item_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - client, err := deps.GetClient(ctx) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - var resp *github.Response - if ownerType == "org" { - resp, err = client.Projects.DeleteOrganizationProjectItem(ctx, owner, projectNumber, itemID) - } else { - resp, err = client.Projects.DeleteUserProjectItem(ctx, owner, projectNumber, itemID) - } - - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - ProjectDeleteFailedError, - resp, - err, - ), nil, nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusNoContent { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("failed to read response body: %w", err) - } - return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, ProjectDeleteFailedError, resp, body), nil, nil - } - return utils.NewToolResultText("project item successfully deleted"), nil, nil - }, - ) - tool.FeatureFlagEnable = FeatureFlagHoldbackConsolidatedProjects - return tool -} +// Method constants for consolidated project tools +const ( + projectsMethodListProjects = "list_projects" + projectsMethodListProjectFields = "list_project_fields" + projectsMethodListProjectItems = "list_project_items" + projectsMethodGetProject = "get_project" + projectsMethodGetProjectField = "get_project_field" + projectsMethodGetProjectItem = "get_project_item" + projectsMethodAddProjectItem = "add_project_item" + projectsMethodUpdateProjectItem = "update_project_item" + projectsMethodDeleteProjectItem = "delete_project_item" +) // ProjectsList returns the tool and handler for listing GitHub Projects resources. func ProjectsList(t translations.TranslationHelperFunc) inventory.ServerTool { @@ -1147,7 +161,6 @@ Use this tool to list projects for a user or organization, or list project field } }, ) - tool.FeatureFlagDisable = FeatureFlagHoldbackConsolidatedProjects return tool } @@ -1267,7 +280,6 @@ Use this tool to get details about individual projects, project fields, and proj } }, ) - tool.FeatureFlagDisable = FeatureFlagHoldbackConsolidatedProjects return tool } @@ -1438,7 +450,6 @@ func ProjectsWrite(t translations.TranslationHelperFunc) inventory.ServerTool { } }, ) - tool.FeatureFlagDisable = FeatureFlagHoldbackConsolidatedProjects return tool } @@ -1959,17 +970,6 @@ type pageInfo struct { PrevCursor string `json:"prevCursor,omitempty"` } -func toNewProjectType(projType string) string { - switch strings.ToLower(projType) { - case "issue": - return "Issue" - case "pull_request": - return "PullRequest" - default: - return "" - } -} - // validateAndConvertToInt64 ensures the value is a number and converts it to int64. func validateAndConvertToInt64(value any) (int64, error) { switch v := value.(type) { diff --git a/pkg/github/projects_test.go b/pkg/github/projects_test.go index 24163ef90..9270da10f 100644 --- a/pkg/github/projects_test.go +++ b/pkg/github/projects_test.go @@ -16,1522 +16,6 @@ import ( "github.com/stretchr/testify/require" ) -func Test_ListProjects(t *testing.T) { - serverTool := ListProjects(translations.NullTranslationHelper) - tool := serverTool.Tool - require.NoError(t, toolsnaps.Test(tool.Name, tool)) - - assert.Equal(t, "list_projects", tool.Name) - assert.NotEmpty(t, tool.Description) - schema, ok := tool.InputSchema.(*jsonschema.Schema) - require.True(t, ok, "InputSchema should be a *jsonschema.Schema") - assert.Contains(t, schema.Properties, "owner") - assert.Contains(t, schema.Properties, "owner_type") - assert.Contains(t, schema.Properties, "query") - assert.Contains(t, schema.Properties, "per_page") - assert.ElementsMatch(t, schema.Required, []string{"owner", "owner_type"}) - - // API returns full ProjectV2 objects; we only need minimal fields for decoding. - orgProjects := []map[string]any{{"id": 1, "node_id": "NODE1", "title": "Org Project"}} - userProjects := []map[string]any{{"id": 2, "node_id": "NODE2", "title": "User Project"}} - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]interface{} - expectError bool - expectedLength int - expectedErrMsg string - }{ - { - name: "success organization", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2: mockResponse(t, http.StatusOK, orgProjects), - }), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - "owner_type": "org", - }, - expectError: false, - expectedLength: 1, - }, - { - name: "success user", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetUsersProjectsV2ByUsername: mockResponse(t, http.StatusOK, userProjects), - }), - requestArgs: map[string]interface{}{ - "owner": "octocat", - "owner_type": "user", - }, - expectError: false, - expectedLength: 1, - }, - { - name: "success organization with pagination & query", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2: expectQueryParams(t, map[string]string{ - "per_page": "50", - "q": "roadmap", - }).andThen(mockResponse(t, http.StatusOK, orgProjects)), - }), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - "owner_type": "org", - "per_page": float64(50), - "query": "roadmap", - }, - expectError: false, - expectedLength: 1, - }, - { - name: "api error", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2: mockResponse(t, http.StatusInternalServerError, map[string]string{"message": "boom"}), - }), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - "owner_type": "org", - }, - expectError: true, - expectedErrMsg: "failed to list projects", - }, - { - name: "missing owner", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]interface{}{ - "owner_type": "org", - }, - expectError: true, - }, - { - name: "missing owner_type", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - }, - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := gh.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := serverTool.Handler(deps) - request := createMCPRequest(tc.requestArgs) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - if tc.expectError { - require.True(t, result.IsError) - text := getTextResult(t, result).Text - if tc.expectedErrMsg != "" { - assert.Contains(t, text, tc.expectedErrMsg) - } - if tc.name == "missing owner" { - assert.Contains(t, text, "missing required parameter: owner") - } - if tc.name == "missing owner_type" { - assert.Contains(t, text, "missing required parameter: owner_type") - } - return - } - - require.False(t, result.IsError) - textContent := getTextResult(t, result) - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - projects, ok := response["projects"].([]interface{}) - require.True(t, ok) - assert.Equal(t, tc.expectedLength, len(projects)) - // pageInfo should exist - _, hasPageInfo := response["pageInfo"].(map[string]interface{}) - assert.True(t, hasPageInfo) - }) - } -} - -func Test_GetProject(t *testing.T) { - serverTool := GetProject(translations.NullTranslationHelper) - tool := serverTool.Tool - require.NoError(t, toolsnaps.Test(tool.Name, tool)) - - assert.Equal(t, "get_project", tool.Name) - assert.NotEmpty(t, tool.Description) - schema, ok := tool.InputSchema.(*jsonschema.Schema) - require.True(t, ok, "InputSchema should be a *jsonschema.Schema") - assert.Contains(t, schema.Properties, "project_number") - assert.Contains(t, schema.Properties, "owner") - assert.Contains(t, schema.Properties, "owner_type") - assert.ElementsMatch(t, schema.Required, []string{"project_number", "owner", "owner_type"}) - - project := map[string]any{"id": 123, "title": "Project Title"} - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]interface{} - expectError bool - expectedErrMsg string - }{ - { - name: "success organization project fetch", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2ByProject: mockResponse(t, http.StatusOK, project), - }), - requestArgs: map[string]interface{}{ - "project_number": float64(123), - "owner": "octo-org", - "owner_type": "org", - }, - expectError: false, - }, - { - name: "success user project fetch", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetUsersProjectsV2ByUsernameByProject: mockResponse(t, http.StatusOK, project), - }), - requestArgs: map[string]interface{}{ - "project_number": float64(456), - "owner": "octocat", - "owner_type": "user", - }, - expectError: false, - }, - { - name: "api error", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2ByProject: mockResponse(t, http.StatusInternalServerError, map[string]string{"message": "boom"}), - }), - requestArgs: map[string]interface{}{ - "project_number": float64(999), - "owner": "octo-org", - "owner_type": "org", - }, - expectError: true, - expectedErrMsg: "failed to get project", - }, - { - name: "missing project_number", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - "owner_type": "org", - }, - expectError: true, - }, - { - name: "missing owner", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]interface{}{ - "project_number": float64(123), - "owner_type": "org", - }, - expectError: true, - }, - { - name: "missing owner_type", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]interface{}{ - "project_number": float64(123), - "owner": "octo-org", - }, - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := gh.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := serverTool.Handler(deps) - request := createMCPRequest(tc.requestArgs) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - if tc.expectError { - require.True(t, result.IsError) - text := getTextResult(t, result).Text - if tc.expectedErrMsg != "" { - assert.Contains(t, text, tc.expectedErrMsg) - } - if tc.name == "missing project_number" { - assert.Contains(t, text, "missing required parameter: project_number") - } - if tc.name == "missing owner" { - assert.Contains(t, text, "missing required parameter: owner") - } - if tc.name == "missing owner_type" { - assert.Contains(t, text, "missing required parameter: owner_type") - } - return - } - - require.False(t, result.IsError) - textContent := getTextResult(t, result) - var arr map[string]any - err = json.Unmarshal([]byte(textContent.Text), &arr) - require.NoError(t, err) - }) - } -} - -func Test_ListProjectFields(t *testing.T) { - serverTool := ListProjectFields(translations.NullTranslationHelper) - tool := serverTool.Tool - require.NoError(t, toolsnaps.Test(tool.Name, tool)) - - assert.Equal(t, "list_project_fields", tool.Name) - assert.NotEmpty(t, tool.Description) - schema, ok := tool.InputSchema.(*jsonschema.Schema) - require.True(t, ok, "InputSchema should be a *jsonschema.Schema") - assert.Contains(t, schema.Properties, "owner_type") - assert.Contains(t, schema.Properties, "owner") - assert.Contains(t, schema.Properties, "project_number") - assert.Contains(t, schema.Properties, "per_page") - assert.ElementsMatch(t, schema.Required, []string{"owner_type", "owner", "project_number"}) - - orgFields := []map[string]any{{"id": 101, "name": "Status", "data_type": "single_select"}} - userFields := []map[string]any{{"id": 201, "name": "Priority", "data_type": "single_select"}} - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]interface{} - expectError bool - expectedLength int - expectedErrMsg string - }{ - { - name: "success organization fields", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2FieldsByProject: mockResponse(t, http.StatusOK, orgFields), - }), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(123), - }, - expectedLength: 1, - }, - { - name: "success user fields with per_page override", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetUsersProjectsV2FieldsByUsernameByProject: expectQueryParams(t, map[string]string{ - "per_page": "50", - }).andThen(mockResponse(t, http.StatusOK, userFields)), - }), - requestArgs: map[string]interface{}{ - "owner": "octocat", - "owner_type": "user", - "project_number": float64(456), - "per_page": float64(50), - }, - expectedLength: 1, - }, - { - name: "api error", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2FieldsByProject: mockResponse(t, http.StatusInternalServerError, map[string]string{"message": "boom"}), - }), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(789), - }, - expectError: true, - expectedErrMsg: "failed to list project fields", - }, - { - name: "missing owner", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]interface{}{ - "owner_type": "org", - "project_number": 10, - }, - expectError: true, - }, - { - name: "missing owner_type", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - "project_number": 10, - }, - expectError: true, - }, - { - name: "missing project_number", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - "owner_type": "org", - }, - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := gh.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := serverTool.Handler(deps) - request := createMCPRequest(tc.requestArgs) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - if tc.expectError { - require.True(t, result.IsError) - text := getTextResult(t, result).Text - if tc.expectedErrMsg != "" { - assert.Contains(t, text, tc.expectedErrMsg) - } - if tc.name == "missing owner" { - assert.Contains(t, text, "missing required parameter: owner") - } - if tc.name == "missing owner_type" { - assert.Contains(t, text, "missing required parameter: owner_type") - } - if tc.name == "missing project_number" { - assert.Contains(t, text, "missing required parameter: project_number") - } - return - } - - require.False(t, result.IsError) - textContent := getTextResult(t, result) - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - fields, ok := response["fields"].([]interface{}) - require.True(t, ok) - assert.Equal(t, tc.expectedLength, len(fields)) - _, hasPageInfo := response["pageInfo"].(map[string]interface{}) - assert.True(t, hasPageInfo) - }) - } -} - -func Test_GetProjectField(t *testing.T) { - serverTool := GetProjectField(translations.NullTranslationHelper) - tool := serverTool.Tool - require.NoError(t, toolsnaps.Test(tool.Name, tool)) - - assert.Equal(t, "get_project_field", tool.Name) - assert.NotEmpty(t, tool.Description) - schema, ok := tool.InputSchema.(*jsonschema.Schema) - require.True(t, ok, "InputSchema should be a *jsonschema.Schema") - assert.Contains(t, schema.Properties, "owner_type") - assert.Contains(t, schema.Properties, "owner") - assert.Contains(t, schema.Properties, "project_number") - assert.Contains(t, schema.Properties, "field_id") - assert.ElementsMatch(t, schema.Required, []string{"owner_type", "owner", "project_number", "field_id"}) - - orgField := map[string]any{"id": 101, "name": "Status", "dataType": "single_select"} - userField := map[string]any{"id": 202, "name": "Priority", "dataType": "single_select"} - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - expectedID int - }{ - { - name: "success organization field", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2FieldsByProjectByFieldID: mockResponse(t, http.StatusOK, orgField), - }), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(123), - "field_id": float64(101), - }, - expectedID: 101, - }, - { - name: "success user field", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetUsersProjectsV2FieldsByUsernameByProjectByFieldID: mockResponse(t, http.StatusOK, userField), - }), - requestArgs: map[string]any{ - "owner": "octocat", - "owner_type": "user", - "project_number": float64(456), - "field_id": float64(202), - }, - expectedID: 202, - }, - { - name: "api error", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2FieldsByProjectByFieldID: mockResponse(t, http.StatusInternalServerError, map[string]string{"message": "boom"}), - }), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(789), - "field_id": float64(303), - }, - expectError: true, - expectedErrMsg: "failed to get project field", - }, - { - name: "missing owner", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner_type": "org", - "project_number": float64(10), - "field_id": float64(1), - }, - expectError: true, - }, - { - name: "missing owner_type", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "project_number": float64(10), - "field_id": float64(1), - }, - expectError: true, - }, - { - name: "missing project_number", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "field_id": float64(1), - }, - expectError: true, - }, - { - name: "missing field_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(10), - }, - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := gh.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := serverTool.Handler(deps) - request := createMCPRequest(tc.requestArgs) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - if tc.expectError { - require.True(t, result.IsError) - text := getTextResult(t, result).Text - if tc.expectedErrMsg != "" { - assert.Contains(t, text, tc.expectedErrMsg) - } - if tc.name == "missing owner" { - assert.Contains(t, text, "missing required parameter: owner") - } - if tc.name == "missing owner_type" { - assert.Contains(t, text, "missing required parameter: owner_type") - } - if tc.name == "missing project_number" { - assert.Contains(t, text, "missing required parameter: project_number") - } - if tc.name == "missing field_id" { - assert.Contains(t, text, "missing required parameter: field_id") - } - return - } - - require.False(t, result.IsError) - textContent := getTextResult(t, result) - var field map[string]any - err = json.Unmarshal([]byte(textContent.Text), &field) - require.NoError(t, err) - if tc.expectedID != 0 { - assert.Equal(t, float64(tc.expectedID), field["id"]) - } - }) - } -} - -func Test_ListProjectItems(t *testing.T) { - serverTool := ListProjectItems(translations.NullTranslationHelper) - tool := serverTool.Tool - require.NoError(t, toolsnaps.Test(tool.Name, tool)) - - assert.Equal(t, "list_project_items", tool.Name) - assert.NotEmpty(t, tool.Description) - schema, ok := tool.InputSchema.(*jsonschema.Schema) - require.True(t, ok, "InputSchema should be a *jsonschema.Schema") - assert.Contains(t, schema.Properties, "owner_type") - assert.Contains(t, schema.Properties, "owner") - assert.Contains(t, schema.Properties, "project_number") - assert.Contains(t, schema.Properties, "query") - assert.Contains(t, schema.Properties, "per_page") - assert.Contains(t, schema.Properties, "fields") - assert.ElementsMatch(t, schema.Required, []string{"owner_type", "owner", "project_number"}) - - orgItems := []map[string]any{ - {"id": 301, "content_type": "Issue", "project_node_id": "PR_1", "fields": []map[string]any{ - {"id": 123, "name": "Status", "data_type": "single_select", "value": "value1"}, - {"id": 456, "name": "Priority", "data_type": "single_select", "value": "value2"}, - }}, - } - userItems := []map[string]any{ - {"id": 401, "content_type": "PullRequest", "project_node_id": "PR_2"}, - {"id": 402, "content_type": "DraftIssue", "project_node_id": "PR_3"}, - } - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]interface{} - expectError bool - expectedLength int - expectedErrMsg string - }{ - { - name: "success organization items", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2ItemsByProject: mockResponse(t, http.StatusOK, orgItems), - }), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(123), - }, - expectedLength: 1, - }, - { - name: "success organization items with fields", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2ItemsByProject: expectQueryParams(t, map[string]string{ - "fields": "123,456,789", - "per_page": "50", - }).andThen(mockResponse(t, http.StatusOK, orgItems)), - }), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(123), - "fields": []interface{}{"123", "456", "789"}, - }, - expectedLength: 1, - }, - { - name: "success user items", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetUsersProjectsV2ItemsByUsernameByProject: mockResponse(t, http.StatusOK, userItems), - }), - requestArgs: map[string]interface{}{ - "owner": "octocat", - "owner_type": "user", - "project_number": float64(456), - }, - expectedLength: 2, - }, - { - name: "success with pagination and query", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2ItemsByProject: expectQueryParams(t, map[string]string{ - "per_page": "50", - "q": "bug", - }).andThen(mockResponse(t, http.StatusOK, orgItems)), - }), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(123), - "per_page": float64(50), - "query": "bug", - }, - expectedLength: 1, - }, - { - name: "api error", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2ItemsByProject: mockResponse(t, http.StatusInternalServerError, map[string]string{"message": "boom"}), - }), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(789), - }, - expectError: true, - expectedErrMsg: ProjectListFailedError, - }, - { - name: "missing owner", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]interface{}{ - "owner_type": "org", - "project_number": float64(10), - }, - expectError: true, - }, - { - name: "missing owner_type", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - "project_number": float64(10), - }, - expectError: true, - }, - { - name: "missing project_number", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]interface{}{ - "owner": "octo-org", - "owner_type": "org", - }, - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := gh.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := serverTool.Handler(deps) - request := createMCPRequest(tc.requestArgs) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - if tc.expectError { - require.True(t, result.IsError) - text := getTextResult(t, result).Text - if tc.expectedErrMsg != "" { - assert.Contains(t, text, tc.expectedErrMsg) - } - if tc.name == "missing owner" { - assert.Contains(t, text, "missing required parameter: owner") - } - if tc.name == "missing owner_type" { - assert.Contains(t, text, "missing required parameter: owner_type") - } - if tc.name == "missing project_number" { - assert.Contains(t, text, "missing required parameter: project_number") - } - return - } - - require.False(t, result.IsError) - textContent := getTextResult(t, result) - var response map[string]any - err = json.Unmarshal([]byte(textContent.Text), &response) - require.NoError(t, err) - items, ok := response["items"].([]interface{}) - require.True(t, ok) - assert.Equal(t, tc.expectedLength, len(items)) - _, hasPageInfo := response["pageInfo"].(map[string]interface{}) - assert.True(t, hasPageInfo) - }) - } -} - -func Test_GetProjectItem(t *testing.T) { - serverTool := GetProjectItem(translations.NullTranslationHelper) - tool := serverTool.Tool - require.NoError(t, toolsnaps.Test(tool.Name, tool)) - - assert.Equal(t, "get_project_item", tool.Name) - assert.NotEmpty(t, tool.Description) - schema, ok := tool.InputSchema.(*jsonschema.Schema) - require.True(t, ok, "InputSchema should be a *jsonschema.Schema") - assert.Contains(t, schema.Properties, "owner_type") - assert.Contains(t, schema.Properties, "owner") - assert.Contains(t, schema.Properties, "project_number") - assert.Contains(t, schema.Properties, "item_id") - assert.Contains(t, schema.Properties, "fields") - assert.ElementsMatch(t, schema.Required, []string{"owner_type", "owner", "project_number", "item_id"}) - - orgItem := map[string]any{ - "id": 301, - "content_type": "Issue", - "project_node_id": "PR_1", - "creator": map[string]any{"login": "octocat"}, - } - userItem := map[string]any{ - "id": 501, - "content_type": "PullRequest", - "project_node_id": "PR_2", - "creator": map[string]any{"login": "jane"}, - } - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - expectedID int - }{ - { - name: "success organization item", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2ItemsByProjectByItemID: mockResponse(t, http.StatusOK, orgItem), - }), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(123), - "item_id": float64(301), - }, - expectedID: 301, - }, - { - name: "success organization item with fields", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2ItemsByProjectByItemID: expectQueryParams(t, map[string]string{ - "fields": "123,456", - }).andThen(mockResponse(t, http.StatusOK, orgItem)), - }), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(123), - "item_id": float64(301), - "fields": []interface{}{"123", "456"}, - }, - expectedID: 301, - }, - { - name: "success user item", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetUsersProjectsV2ItemsByUsernameByProjectByItemID: mockResponse(t, http.StatusOK, userItem), - }), - requestArgs: map[string]any{ - "owner": "octocat", - "owner_type": "user", - "project_number": float64(456), - "item_id": float64(501), - }, - expectedID: 501, - }, - { - name: "api error", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - GetOrgsProjectsV2ItemsByProjectByItemID: mockResponse(t, http.StatusInternalServerError, map[string]string{"message": "boom"}), - }), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(789), - "item_id": float64(999), - }, - expectError: true, - expectedErrMsg: "failed to get project item", - }, - { - name: "missing owner", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner_type": "org", - "project_number": float64(10), - "item_id": float64(1), - }, - expectError: true, - }, - { - name: "missing owner_type", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "project_number": float64(10), - "item_id": float64(1), - }, - expectError: true, - }, - { - name: "missing project_number", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "item_id": float64(1), - }, - expectError: true, - }, - { - name: "missing item_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(10), - }, - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := gh.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := serverTool.Handler(deps) - request := createMCPRequest(tc.requestArgs) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - if tc.expectError { - require.True(t, result.IsError) - text := getTextResult(t, result).Text - if tc.expectedErrMsg != "" { - assert.Contains(t, text, tc.expectedErrMsg) - } - if tc.name == "missing owner" { - assert.Contains(t, text, "missing required parameter: owner") - } - if tc.name == "missing owner_type" { - assert.Contains(t, text, "missing required parameter: owner_type") - } - if tc.name == "missing project_number" { - assert.Contains(t, text, "missing required parameter: project_number") - } - if tc.name == "missing item_id" { - assert.Contains(t, text, "missing required parameter: item_id") - } - return - } - - require.False(t, result.IsError) - textContent := getTextResult(t, result) - var item map[string]any - err = json.Unmarshal([]byte(textContent.Text), &item) - require.NoError(t, err) - if tc.expectedID != 0 { - assert.Equal(t, float64(tc.expectedID), item["id"]) - } - }) - } -} - -func Test_AddProjectItem(t *testing.T) { - serverTool := AddProjectItem(translations.NullTranslationHelper) - tool := serverTool.Tool - require.NoError(t, toolsnaps.Test(tool.Name, tool)) - - assert.Equal(t, "add_project_item", tool.Name) - assert.NotEmpty(t, tool.Description) - schema, ok := tool.InputSchema.(*jsonschema.Schema) - require.True(t, ok, "InputSchema should be a *jsonschema.Schema") - assert.Contains(t, schema.Properties, "owner_type") - assert.Contains(t, schema.Properties, "owner") - assert.Contains(t, schema.Properties, "project_number") - assert.Contains(t, schema.Properties, "item_type") - assert.Contains(t, schema.Properties, "item_id") - assert.ElementsMatch(t, schema.Required, []string{"owner_type", "owner", "project_number", "item_type", "item_id"}) - - orgItem := map[string]any{ - "id": 601, - "content_type": "Issue", - "creator": map[string]any{ - "login": "octocat", - "id": 1, - "html_url": "https://github.com/octocat", - "avatar_url": "https://avatars.githubusercontent.com/u/1?v=4", - }, - } - - userItem := map[string]any{ - "id": 701, - "content_type": "PullRequest", - "creator": map[string]any{ - "login": "hubot", - "id": 2, - "html_url": "https://github.com/hubot", - "avatar_url": "https://avatars.githubusercontent.com/u/2?v=4", - }, - } - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - expectedID int - expectedContentType string - expectedCreatorLogin string - }{ - { - name: "success organization issue", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - PostOrgsProjectsV2ItemsByProject: expectRequestBody(t, map[string]any{ - "type": "Issue", - "id": float64(9876), - }).andThen(mockResponse(t, http.StatusCreated, orgItem)), - }), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(321), - "item_type": "issue", - "item_id": float64(9876), - }, - expectedID: 601, - expectedContentType: "Issue", - expectedCreatorLogin: "octocat", - }, - { - name: "success user pull request", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - PostUsersProjectsV2ItemsByUsernameByProject: expectRequestBody(t, map[string]any{ - "type": "PullRequest", - "id": float64(7654), - }).andThen(mockResponse(t, http.StatusCreated, userItem)), - }), - requestArgs: map[string]any{ - "owner": "octocat", - "owner_type": "user", - "project_number": float64(222), - "item_type": "pull_request", - "item_id": float64(7654), - }, - expectedID: 701, - expectedContentType: "PullRequest", - expectedCreatorLogin: "hubot", - }, - { - name: "api error", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - PostOrgsProjectsV2ItemsByProject: mockResponse(t, http.StatusInternalServerError, map[string]string{"message": "boom"}), - }), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(999), - "item_type": "issue", - "item_id": float64(8888), - }, - expectError: true, - expectedErrMsg: ProjectAddFailedError, - }, - { - name: "missing owner", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner_type": "org", - "project_number": float64(1), - "item_type": "Issue", - "item_id": float64(10), - }, - expectError: true, - }, - { - name: "missing owner_type", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "project_number": float64(1), - "item_type": "Issue", - "item_id": float64(10), - }, - expectError: true, - }, - { - name: "missing project_number", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "item_type": "Issue", - "item_id": float64(10), - }, - expectError: true, - }, - { - name: "missing item_type", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(1), - "item_id": float64(10), - }, - expectError: true, - }, - { - name: "missing item_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(1), - "item_type": "Issue", - }, - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := gh.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := serverTool.Handler(deps) - request := createMCPRequest(tc.requestArgs) - - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - require.NoError(t, err) - - if tc.expectError { - require.True(t, result.IsError) - text := getTextResult(t, result).Text - if tc.expectedErrMsg != "" { - assert.Contains(t, text, tc.expectedErrMsg) - } - switch tc.name { - case "missing owner": - assert.Contains(t, text, "missing required parameter: owner") - case "missing owner_type": - assert.Contains(t, text, "missing required parameter: owner_type") - case "missing project_number": - assert.Contains(t, text, "missing required parameter: project_number") - case "missing item_type": - assert.Contains(t, text, "missing required parameter: item_type") - case "missing item_id": - assert.Contains(t, text, "missing required parameter: item_id") - // case "api error": - // assert.Contains(t, text, ProjectAddFailedError) - } - return - } - - require.False(t, result.IsError) - textContent := getTextResult(t, result) - var item map[string]any - require.NoError(t, json.Unmarshal([]byte(textContent.Text), &item)) - if tc.expectedID != 0 { - assert.Equal(t, float64(tc.expectedID), item["id"]) - } - if tc.expectedContentType != "" { - assert.Equal(t, tc.expectedContentType, item["content_type"]) - } - if tc.expectedCreatorLogin != "" { - creator, ok := item["creator"].(map[string]any) - require.True(t, ok) - assert.Equal(t, tc.expectedCreatorLogin, creator["login"]) - } - }) - } -} - -func Test_UpdateProjectItem(t *testing.T) { - serverTool := UpdateProjectItem(translations.NullTranslationHelper) - tool := serverTool.Tool - require.NoError(t, toolsnaps.Test(tool.Name, tool)) - - assert.Equal(t, "update_project_item", tool.Name) - assert.NotEmpty(t, tool.Description) - schema, ok := tool.InputSchema.(*jsonschema.Schema) - require.True(t, ok, "InputSchema should be a *jsonschema.Schema") - assert.Contains(t, schema.Properties, "owner_type") - assert.Contains(t, schema.Properties, "owner") - assert.Contains(t, schema.Properties, "project_number") - assert.Contains(t, schema.Properties, "item_id") - assert.Contains(t, schema.Properties, "updated_field") - assert.ElementsMatch(t, schema.Required, []string{"owner_type", "owner", "project_number", "item_id", "updated_field"}) - - orgUpdatedItem := map[string]any{ - "id": 801, - "content_type": "Issue", - } - userUpdatedItem := map[string]any{ - "id": 802, - "content_type": "PullRequest", - } - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - expectedID int - }{ - { - name: "success organization update", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - PatchOrgsProjectsV2ItemsByProjectByItemID: expectRequestBody(t, map[string]any{ - "fields": []any{map[string]any{"id": float64(101), "value": "Done"}}, - }).andThen(mockResponse(t, http.StatusOK, orgUpdatedItem)), - }), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(1001), - "item_id": float64(5555), - "updated_field": map[string]any{ - "id": float64(101), - "value": "Done", - }, - }, - expectedID: 801, - }, - { - name: "success user update", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - PatchUsersProjectsV2ItemsByUsernameByProjectByItemID: expectRequestBody(t, map[string]any{ - "fields": []any{map[string]any{"id": float64(202), "value": float64(42)}}, - }).andThen(mockResponse(t, http.StatusOK, userUpdatedItem)), - }), - requestArgs: map[string]any{ - "owner": "octocat", - "owner_type": "user", - "project_number": float64(2002), - "item_id": float64(6666), - "updated_field": map[string]any{ - "id": float64(202), - "value": float64(42), - }, - }, - expectedID: 802, - }, - { - name: "api error", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - PatchOrgsProjectsV2ItemsByProjectByItemID: mockResponse(t, http.StatusInternalServerError, map[string]string{"message": "boom"}), - }), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(3003), - "item_id": float64(7777), - "updated_field": map[string]any{ - "id": float64(303), - "value": "In Progress", - }, - }, - expectError: true, - expectedErrMsg: "failed to update a project item", - }, - { - name: "missing owner", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner_type": "org", - "project_number": float64(1), - "item_id": float64(2), - "updated_field": map[string]any{ - "id": float64(1), - "value": "X", - }, - }, - expectError: true, - }, - { - name: "missing owner_type", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "project_number": float64(1), - "item_id": float64(2), - "updated_field": map[string]any{ - "id": float64(1), - "value": "X", - }, - }, - expectError: true, - }, - { - name: "missing project_number", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "item_id": float64(2), - "updated_field": map[string]any{ - "id": float64(1), - "value": "X", - }, - }, - expectError: true, - }, - { - name: "missing item_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(1), - "updated_field": map[string]any{ - "id": float64(1), - "value": "X", - }, - }, - expectError: true, - }, - { - name: "missing updated_field", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(1), - "item_id": float64(2), - }, - expectError: true, - }, - { - name: "updated_field not object", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(1), - "item_id": float64(2), - "updated_field": "not-an-object", - }, - expectError: true, - }, - { - name: "updated_field missing id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(1), - "item_id": float64(2), - "updated_field": map[string]any{}, - }, - expectError: true, - }, - { - name: "updated_field missing value", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(1), - "item_id": float64(2), - "updated_field": map[string]any{ - "id": float64(9), - }, - }, - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := gh.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := serverTool.Handler(deps) - request := createMCPRequest(tc.requestArgs) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - if tc.expectError { - require.True(t, result.IsError) - text := getTextResult(t, result).Text - if tc.expectedErrMsg != "" { - assert.Contains(t, text, tc.expectedErrMsg) - } - switch tc.name { - case "missing owner": - assert.Contains(t, text, "missing required parameter: owner") - case "missing owner_type": - assert.Contains(t, text, "missing required parameter: owner_type") - case "missing project_number": - assert.Contains(t, text, "missing required parameter: project_number") - case "missing item_id": - assert.Contains(t, text, "missing required parameter: item_id") - case "missing updated_field": - assert.Contains(t, text, "missing required parameter: updated_field") - case "updated_field not object": - assert.Contains(t, text, "field_value must be an object") - case "updated_field missing id": - assert.Contains(t, text, "updated_field.id is required") - case "updated_field missing value": - assert.Contains(t, text, "updated_field.value is required") - } - return - } - - require.False(t, result.IsError) - textContent := getTextResult(t, result) - var item map[string]any - require.NoError(t, json.Unmarshal([]byte(textContent.Text), &item)) - if tc.expectedID != 0 { - assert.Equal(t, float64(tc.expectedID), item["id"]) - } - }) - } -} - -func Test_DeleteProjectItem(t *testing.T) { - serverTool := DeleteProjectItem(translations.NullTranslationHelper) - tool := serverTool.Tool - require.NoError(t, toolsnaps.Test(tool.Name, tool)) - - assert.Equal(t, "delete_project_item", tool.Name) - assert.NotEmpty(t, tool.Description) - schema, ok := tool.InputSchema.(*jsonschema.Schema) - require.True(t, ok, "InputSchema should be a *jsonschema.Schema") - assert.Contains(t, schema.Properties, "owner_type") - assert.Contains(t, schema.Properties, "owner") - assert.Contains(t, schema.Properties, "project_number") - assert.Contains(t, schema.Properties, "item_id") - assert.ElementsMatch(t, schema.Required, []string{"owner_type", "owner", "project_number", "item_id"}) - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]any - expectError bool - expectedErrMsg string - expectedText string - }{ - { - name: "success organization delete", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - DeleteOrgsProjectsV2ItemsByProjectByItemID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNoContent) - }), - }), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(123), - "item_id": float64(555), - }, - expectedText: "project item successfully deleted", - }, - { - name: "success user delete", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - DeleteUsersProjectsV2ItemsByUsernameByProjectByItemID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNoContent) - }), - }), - requestArgs: map[string]any{ - "owner": "octocat", - "owner_type": "user", - "project_number": float64(456), - "item_id": float64(777), - }, - expectedText: "project item successfully deleted", - }, - { - name: "api error", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ - DeleteOrgsProjectsV2ItemsByProjectByItemID: mockResponse(t, http.StatusInternalServerError, map[string]string{"message": "boom"}), - }), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(321), - "item_id": float64(999), - }, - expectError: true, - expectedErrMsg: ProjectDeleteFailedError, - }, - { - name: "missing owner", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner_type": "org", - "project_number": float64(1), - "item_id": float64(10), - }, - expectError: true, - }, - { - name: "missing owner_type", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "project_number": float64(1), - "item_id": float64(10), - }, - expectError: true, - }, - { - name: "missing project_number", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "item_id": float64(10), - }, - expectError: true, - }, - { - name: "missing item_id", - mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), - requestArgs: map[string]any{ - "owner": "octo-org", - "owner_type": "org", - "project_number": float64(1), - }, - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - client := gh.NewClient(tc.mockedClient) - deps := BaseDeps{ - Client: client, - } - handler := serverTool.Handler(deps) - request := createMCPRequest(tc.requestArgs) - result, err := handler(ContextWithDeps(context.Background(), deps), &request) - - require.NoError(t, err) - if tc.expectError { - require.True(t, result.IsError) - text := getTextResult(t, result).Text - if tc.expectedErrMsg != "" { - assert.Contains(t, text, tc.expectedErrMsg) - } - switch tc.name { - case "missing owner": - assert.Contains(t, text, "missing required parameter: owner") - case "missing owner_type": - assert.Contains(t, text, "missing required parameter: owner_type") - case "missing project_number": - assert.Contains(t, text, "missing required parameter: project_number") - case "missing item_id": - assert.Contains(t, text, "missing required parameter: item_id") - } - return - } - - require.False(t, result.IsError) - text := getTextResult(t, result).Text - assert.Contains(t, text, tc.expectedText) - }) - } -} - // Tests for consolidated project tools func Test_ProjectsList(t *testing.T) { diff --git a/pkg/github/tools.go b/pkg/github/tools.go index a169ff591..e924456fb 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -241,21 +241,6 @@ func AllTools(t translations.TranslationHelperFunc) []inventory.ServerTool { ListDiscussionCategories(t), // Actions tools - ListWorkflows(t), - ListWorkflowRuns(t), - GetWorkflowRun(t), - GetWorkflowRunLogs(t), - ListWorkflowJobs(t), - GetJobLogs(t), - ListWorkflowRunArtifacts(t), - DownloadWorkflowRunArtifact(t), - GetWorkflowRunUsage(t), - RunWorkflow(t), - RerunWorkflowRun(t), - RerunFailedJobs(t), - CancelWorkflowRun(t), - DeleteWorkflowRunLogs(t), - // Consolidated Actions tools (enabled via feature flag) ActionsList(t), ActionsGet(t), ActionsRunTrigger(t), @@ -274,17 +259,6 @@ func AllTools(t translations.TranslationHelperFunc) []inventory.ServerTool { UpdateGist(t), // Project tools - ListProjects(t), - GetProject(t), - ListProjectFields(t), - GetProjectField(t), - ListProjectItems(t), - GetProjectItem(t), - AddProjectItem(t), - DeleteProjectItem(t), - UpdateProjectItem(t), - - // Consolidated project tools (enabled via feature flag) ProjectsList(t), ProjectsGet(t), ProjectsWrite(t),