diff --git a/README.md b/README.md
index b806d29ff..93e6af03f 100644
--- a/README.md
+++ b/README.md
@@ -490,94 +490,49 @@ The following sets of tools are available:
Actions
-- **cancel_workflow_run** - Cancel workflow run
+- **actions_get** - Get details of GitHub Actions resources (workflows, workflow runs, jobs, and artifacts)
+ - `method`: The method to execute (string, required)
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- - `run_id`: The unique identifier of the workflow run (number, required)
+ - `resource_id`: The unique identifier of the resource. This will vary based on the "method" provided, so ensure you provide the correct ID:
+ - Provide a workflow ID or workflow file name (e.g. ci.yaml) for 'get_workflow' method.
+ - Provide a workflow run ID for 'get_workflow_run', 'get_workflow_run_usage', and 'get_workflow_run_logs_url' methods.
+ - Provide an artifact ID for 'download_workflow_run_artifact' method.
+ - Provide a job ID for 'get_workflow_job' method.
+ (string, required)
-- **delete_workflow_run_logs** - Delete workflow logs
+- **actions_list** - List GitHub Actions workflows in a repository
+ - `method`: The action to perform (string, required)
- `owner`: Repository owner (string, required)
+ - `page`: Page number for pagination (default: 1) (number, optional)
+ - `per_page`: Results per page for pagination (default: 30, max: 100) (number, optional)
- `repo`: Repository name (string, required)
- - `run_id`: The unique identifier of the workflow run (number, required)
-
-- **download_workflow_run_artifact** - Download workflow artifact
- - `artifact_id`: The unique identifier of the artifact (number, required)
+ - `resource_id`: The unique identifier of the resource. This will vary based on the "method" provided, so ensure you provide the correct ID:
+ - Do not provide any resource ID for 'list_workflows' method.
+ - Provide a workflow ID or workflow file name (e.g. ci.yaml) for 'list_workflow_runs' method.
+ - Provide a workflow run ID for 'list_workflow_jobs' and 'list_workflow_run_artifacts' methods.
+ (string, optional)
+ - `workflow_jobs_filter`: Filters for workflow jobs. **ONLY** used when method is 'list_workflow_jobs' (object, optional)
+ - `workflow_runs_filter`: Filters for workflow runs. **ONLY** used when method is 'list_workflow_runs' (object, optional)
+
+- **actions_run_trigger** - Trigger GitHub Actions workflow actions
+ - `inputs`: Inputs the workflow accepts. Only used for 'run_workflow' method. (object, optional)
+ - `method`: The method to execute (string, required)
- `owner`: Repository owner (string, required)
+ - `ref`: The git reference for the workflow. The reference can be a branch or tag name. Required for 'run_workflow' method. (string, optional)
- `repo`: Repository name (string, required)
+ - `run_id`: The ID of the workflow run. Required for all methods except 'run_workflow'. (number, optional)
+ - `workflow_id`: The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml). Required for 'run_workflow' method. (string, optional)
-- **get_job_logs** - Get job logs
- - `failed_only`: When true, gets logs for all failed jobs in run_id (boolean, optional)
- - `job_id`: The unique identifier of the workflow job (required for single job logs) (number, optional)
+- **get_job_logs** - Get GitHub Actions workflow job logs
+ - `failed_only`: When true, gets logs for all failed jobs in the workflow run specified by run_id. Requires run_id to be provided. (boolean, optional)
+ - `job_id`: The unique identifier of the workflow job. Required when getting logs for a single job. (number, optional)
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `return_content`: Returns actual log content instead of URLs (boolean, optional)
- - `run_id`: Workflow run ID (required when using failed_only) (number, optional)
+ - `run_id`: The unique identifier of the workflow run. Required when failed_only is true to get logs for all failed jobs in the run. (number, optional)
- `tail_lines`: Number of lines to return from the end of the log (number, optional)
-- **get_workflow_run** - Get workflow run
- - `owner`: Repository owner (string, required)
- - `repo`: Repository name (string, required)
- - `run_id`: The unique identifier of the workflow run (number, required)
-
-- **get_workflow_run_logs** - Get workflow run logs
- - `owner`: Repository owner (string, required)
- - `repo`: Repository name (string, required)
- - `run_id`: The unique identifier of the workflow run (number, required)
-
-- **get_workflow_run_usage** - Get workflow usage
- - `owner`: Repository owner (string, required)
- - `repo`: Repository name (string, required)
- - `run_id`: The unique identifier of the workflow run (number, required)
-
-- **list_workflow_jobs** - List workflow jobs
- - `filter`: Filters jobs by their completed_at timestamp (string, optional)
- - `owner`: Repository owner (string, required)
- - `page`: Page number for pagination (min 1) (number, optional)
- - `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- - `repo`: Repository name (string, required)
- - `run_id`: The unique identifier of the workflow run (number, required)
-
-- **list_workflow_run_artifacts** - List workflow artifacts
- - `owner`: Repository owner (string, required)
- - `page`: Page number for pagination (min 1) (number, optional)
- - `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- - `repo`: Repository name (string, required)
- - `run_id`: The unique identifier of the workflow run (number, required)
-
-- **list_workflow_runs** - List workflow runs
- - `actor`: Returns someone's workflow runs. Use the login for the user who created the workflow run. (string, optional)
- - `branch`: Returns workflow runs associated with a branch. Use the name of the branch. (string, optional)
- - `event`: Returns workflow runs for a specific event type (string, optional)
- - `owner`: Repository owner (string, required)
- - `page`: Page number for pagination (min 1) (number, optional)
- - `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- - `repo`: Repository name (string, required)
- - `status`: Returns workflow runs with the check run status (string, optional)
- - `workflow_id`: The workflow ID or workflow file name (string, required)
-
-- **list_workflows** - List workflows
- - `owner`: Repository owner (string, required)
- - `page`: Page number for pagination (min 1) (number, optional)
- - `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- - `repo`: Repository name (string, required)
-
-- **rerun_failed_jobs** - Rerun failed jobs
- - `owner`: Repository owner (string, required)
- - `repo`: Repository name (string, required)
- - `run_id`: The unique identifier of the workflow run (number, required)
-
-- **rerun_workflow_run** - Rerun workflow run
- - `owner`: Repository owner (string, required)
- - `repo`: Repository name (string, required)
- - `run_id`: The unique identifier of the workflow run (number, required)
-
-- **run_workflow** - Run workflow
- - `inputs`: Inputs the workflow accepts (object, optional)
- - `owner`: Repository owner (string, required)
- - `ref`: The git reference for the workflow. The reference can be a branch or tag name. (string, required)
- - `repo`: Repository name (string, required)
- - `workflow_id`: The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml) (string, required)
-
diff --git a/pkg/github/__toolsnaps__/actions_get.snap b/pkg/github/__toolsnaps__/actions_get.snap
new file mode 100644
index 000000000..b5f3b85bd
--- /dev/null
+++ b/pkg/github/__toolsnaps__/actions_get.snap
@@ -0,0 +1,43 @@
+{
+ "annotations": {
+ "readOnlyHint": true,
+ "title": "Get details of GitHub Actions resources (workflows, workflow runs, jobs, and artifacts)"
+ },
+ "description": "Get details about specific GitHub Actions resources.\nUse this tool to get details about individual workflows, workflow runs, jobs, and artifacts by their unique IDs.\n",
+ "inputSchema": {
+ "type": "object",
+ "required": [
+ "method",
+ "owner",
+ "repo",
+ "resource_id"
+ ],
+ "properties": {
+ "method": {
+ "type": "string",
+ "description": "The method to execute",
+ "enum": [
+ "get_workflow",
+ "get_workflow_run",
+ "get_workflow_job",
+ "download_workflow_run_artifact",
+ "get_workflow_run_usage",
+ "get_workflow_run_logs_url"
+ ]
+ },
+ "owner": {
+ "type": "string",
+ "description": "Repository owner"
+ },
+ "repo": {
+ "type": "string",
+ "description": "Repository name"
+ },
+ "resource_id": {
+ "type": "string",
+ "description": "The unique identifier of the resource. This will vary based on the \"method\" provided, so ensure you provide the correct ID:\n- Provide a workflow ID or workflow file name (e.g. ci.yaml) for 'get_workflow' method.\n- Provide a workflow run ID for 'get_workflow_run', 'get_workflow_run_usage', and 'get_workflow_run_logs_url' methods.\n- Provide an artifact ID for 'download_workflow_run_artifact' method.\n- Provide a job ID for 'get_workflow_job' method.\n"
+ }
+ }
+ },
+ "name": "actions_get"
+}
\ No newline at end of file
diff --git a/pkg/github/__toolsnaps__/actions_list.snap b/pkg/github/__toolsnaps__/actions_list.snap
new file mode 100644
index 000000000..3968a6eae
--- /dev/null
+++ b/pkg/github/__toolsnaps__/actions_list.snap
@@ -0,0 +1,128 @@
+{
+ "annotations": {
+ "readOnlyHint": true,
+ "title": "List GitHub Actions workflows in a repository"
+ },
+ "description": "Tools for listing GitHub Actions resources.\nUse this tool to list workflows in a repository, or list workflow runs, jobs, and artifacts for a specific workflow or workflow run.\n",
+ "inputSchema": {
+ "type": "object",
+ "required": [
+ "method",
+ "owner",
+ "repo"
+ ],
+ "properties": {
+ "method": {
+ "type": "string",
+ "description": "The action to perform",
+ "enum": [
+ "list_workflows",
+ "list_workflow_runs",
+ "list_workflow_jobs",
+ "list_workflow_run_artifacts"
+ ]
+ },
+ "owner": {
+ "type": "string",
+ "description": "Repository owner"
+ },
+ "page": {
+ "type": "number",
+ "description": "Page number for pagination (default: 1)",
+ "minimum": 1
+ },
+ "per_page": {
+ "type": "number",
+ "description": "Results per page for pagination (default: 30, max: 100)",
+ "minimum": 1,
+ "maximum": 100
+ },
+ "repo": {
+ "type": "string",
+ "description": "Repository name"
+ },
+ "resource_id": {
+ "type": "string",
+ "description": "The unique identifier of the resource. This will vary based on the \"method\" provided, so ensure you provide the correct ID:\n- Do not provide any resource ID for 'list_workflows' method.\n- Provide a workflow ID or workflow file name (e.g. ci.yaml) for 'list_workflow_runs' method.\n- Provide a workflow run ID for 'list_workflow_jobs' and 'list_workflow_run_artifacts' methods.\n"
+ },
+ "workflow_jobs_filter": {
+ "type": "object",
+ "description": "Filters for workflow jobs. **ONLY** used when method is 'list_workflow_jobs'",
+ "properties": {
+ "filter": {
+ "type": "string",
+ "description": "Filters jobs by their completed_at timestamp",
+ "enum": [
+ "latest",
+ "all"
+ ]
+ }
+ }
+ },
+ "workflow_runs_filter": {
+ "type": "object",
+ "description": "Filters for workflow runs. **ONLY** used when method is 'list_workflow_runs'",
+ "properties": {
+ "actor": {
+ "type": "string",
+ "description": "Filter to a specific GitHub user's workflow runs."
+ },
+ "branch": {
+ "type": "string",
+ "description": "Filter workflow runs to a specific Git branch. Use the name of the branch."
+ },
+ "event": {
+ "type": "string",
+ "description": "Filter workflow runs to 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"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "Filter workflow runs to only runs with a specific status",
+ "enum": [
+ "queued",
+ "in_progress",
+ "completed",
+ "requested",
+ "waiting"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "name": "actions_list"
+}
\ No newline at end of file
diff --git a/pkg/github/__toolsnaps__/actions_run_trigger.snap b/pkg/github/__toolsnaps__/actions_run_trigger.snap
new file mode 100644
index 000000000..4e16f8958
--- /dev/null
+++ b/pkg/github/__toolsnaps__/actions_run_trigger.snap
@@ -0,0 +1,53 @@
+{
+ "annotations": {
+ "destructiveHint": true,
+ "title": "Trigger GitHub Actions workflow actions"
+ },
+ "description": "Trigger GitHub Actions workflow operations, including running, re-running, cancelling workflow runs, and deleting workflow run logs.",
+ "inputSchema": {
+ "type": "object",
+ "required": [
+ "method",
+ "owner",
+ "repo"
+ ],
+ "properties": {
+ "inputs": {
+ "type": "object",
+ "description": "Inputs the workflow accepts. Only used for 'run_workflow' method."
+ },
+ "method": {
+ "type": "string",
+ "description": "The method to execute",
+ "enum": [
+ "run_workflow",
+ "rerun_workflow_run",
+ "rerun_failed_jobs",
+ "cancel_workflow_run",
+ "delete_workflow_run_logs"
+ ]
+ },
+ "owner": {
+ "type": "string",
+ "description": "Repository owner"
+ },
+ "ref": {
+ "type": "string",
+ "description": "The git reference for the workflow. The reference can be a branch or tag name. Required for 'run_workflow' method."
+ },
+ "repo": {
+ "type": "string",
+ "description": "Repository name"
+ },
+ "run_id": {
+ "type": "number",
+ "description": "The ID of the workflow run. Required for all methods except 'run_workflow'."
+ },
+ "workflow_id": {
+ "type": "string",
+ "description": "The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml). Required for 'run_workflow' method."
+ }
+ }
+ },
+ "name": "actions_run_trigger"
+}
\ No newline at end of file
diff --git a/pkg/github/__toolsnaps__/get_job_logs.snap b/pkg/github/__toolsnaps__/get_job_logs.snap
index 8b2319527..23e2b640f 100644
--- a/pkg/github/__toolsnaps__/get_job_logs.snap
+++ b/pkg/github/__toolsnaps__/get_job_logs.snap
@@ -1,9 +1,9 @@
{
"annotations": {
"readOnlyHint": true,
- "title": "Get job logs"
+ "title": "Get GitHub Actions workflow job logs"
},
- "description": "Download logs for a specific workflow job or efficiently get all failed job logs for a workflow run",
+ "description": "Get logs for GitHub Actions workflow jobs.\nUse this tool to retrieve logs for a specific job or all failed jobs in a workflow run.\nFor single job logs, provide job_id. For all failed jobs in a run, provide run_id with failed_only=true.\n",
"inputSchema": {
"type": "object",
"required": [
@@ -13,11 +13,11 @@
"properties": {
"failed_only": {
"type": "boolean",
- "description": "When true, gets logs for all failed jobs in run_id"
+ "description": "When true, gets logs for all failed jobs in the workflow run specified by run_id. Requires run_id to be provided."
},
"job_id": {
"type": "number",
- "description": "The unique identifier of the workflow job (required for single job logs)"
+ "description": "The unique identifier of the workflow job. Required when getting logs for a single job."
},
"owner": {
"type": "string",
@@ -33,7 +33,7 @@
},
"run_id": {
"type": "number",
- "description": "Workflow run ID (required when using failed_only)"
+ "description": "The unique identifier of the workflow run. Required when failed_only is true to get logs for all failed jobs in the run."
},
"tail_lines": {
"type": "number",
diff --git a/pkg/github/actions.go b/pkg/github/actions.go
index 81ed55296..453be8f98 100644
--- a/pkg/github/actions.go
+++ b/pkg/github/actions.go
@@ -3,6 +3,7 @@ package github
import (
"context"
"encoding/json"
+ "errors"
"fmt"
"net/http"
"strconv"
@@ -18,189 +19,169 @@ import (
"github.com/modelcontextprotocol/go-sdk/mcp"
)
+// Method constants for consolidated actions tools
const (
- DescriptionRepositoryOwner = "Repository owner"
- DescriptionRepositoryName = "Repository name"
+ actionsMethodListWorkflows = "list_workflows"
+ actionsMethodListWorkflowRuns = "list_workflow_runs"
+ actionsMethodListWorkflowJobs = "list_workflow_jobs"
+ actionsMethodListWorkflowArtifacts = "list_workflow_run_artifacts"
+ actionsMethodGetWorkflow = "get_workflow"
+ actionsMethodGetWorkflowRun = "get_workflow_run"
+ actionsMethodGetWorkflowJob = "get_workflow_job"
+ actionsMethodGetWorkflowRunUsage = "get_workflow_run_usage"
+ actionsMethodGetWorkflowRunLogsURL = "get_workflow_run_logs_url"
+ actionsMethodDownloadWorkflowArtifact = "download_workflow_run_artifact"
+ actionsMethodRunWorkflow = "run_workflow"
+ actionsMethodRerunWorkflowRun = "rerun_workflow_run"
+ actionsMethodRerunFailedJobs = "rerun_failed_jobs"
+ actionsMethodCancelWorkflowRun = "cancel_workflow_run"
+ actionsMethodDeleteWorkflowRunLogs = "delete_workflow_run_logs"
)
-// ListWorkflows creates a tool to list workflows in a repository
-func ListWorkflows(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
+// ActionsList returns the tool and handler for listing GitHub Actions resources.
+func ActionsList(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
return mcp.Tool{
- Name: "list_workflows",
- Description: t("TOOL_LIST_WORKFLOWS_DESCRIPTION", "List workflows in a repository"),
+ Name: "actions_list",
+ Description: t("TOOL_ACTIONS_LIST_DESCRIPTION", `Tools for listing GitHub Actions resources.
+Use this tool to list workflows in a repository, or list workflow runs, jobs, and artifacts for a specific workflow or workflow run.
+`),
Annotations: &mcp.ToolAnnotations{
- Title: t("TOOL_LIST_WORKFLOWS_USER_TITLE", "List workflows"),
+ Title: t("TOOL_ACTIONS_LIST_USER_TITLE", "List GitHub Actions workflows in a repository"),
ReadOnlyHint: true,
},
- InputSchema: WithPagination(&jsonschema.Schema{
+ InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
- "owner": {
- Type: "string",
- Description: DescriptionRepositoryOwner,
- },
- "repo": {
+ "method": {
Type: "string",
- Description: DescriptionRepositoryName,
+ Description: "The action to perform",
+ Enum: []any{
+ actionsMethodListWorkflows,
+ actionsMethodListWorkflowRuns,
+ actionsMethodListWorkflowJobs,
+ actionsMethodListWorkflowArtifacts,
+ },
},
- },
- Required: []string{"owner", "repo"},
- }),
- },
- func(ctx context.Context, _ *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
- }
- repo, err := RequiredParam[string](args, "repo")
- 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
- }
-
- client, err := getClient(ctx)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
- }
-
- // Set up list options
- opts := &github.ListOptions{
- PerPage: pagination.PerPage,
- Page: pagination.Page,
- }
-
- 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() }()
-
- r, err := json.Marshal(workflows)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
- }
-
- return utils.NewToolResultText(string(r)), nil, nil
- }
-}
-
-// ListWorkflowRuns creates a tool to list workflow runs for a specific workflow
-func ListWorkflowRuns(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
- return 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,
+ Description: "Repository owner",
},
"repo": {
Type: "string",
- Description: DescriptionRepositoryName,
- },
- "workflow_id": {
- Type: "string",
- Description: "The workflow ID or workflow file name",
+ Description: "Repository name",
},
- "actor": {
- Type: "string",
- Description: "Returns someone's workflow runs. Use the login for the user who created the workflow run.",
+ "resource_id": {
+ Type: "string",
+ Description: `The unique identifier of the resource. This will vary based on the "method" provided, so ensure you provide the correct ID:
+- Do not provide any resource ID for 'list_workflows' method.
+- Provide a workflow ID or workflow file name (e.g. ci.yaml) for 'list_workflow_runs' method.
+- Provide a workflow run ID for 'list_workflow_jobs' and 'list_workflow_run_artifacts' methods.
+`,
},
- "branch": {
- Type: "string",
- Description: "Returns workflow runs associated with a branch. Use the name of the branch.",
+ "workflow_runs_filter": {
+ Type: "object",
+ Description: "Filters for workflow runs. **ONLY** used when method is 'list_workflow_runs'",
+ Properties: map[string]*jsonschema.Schema{
+ "actor": {
+ Type: "string",
+ Description: "Filter to a specific GitHub user's workflow runs.",
+ },
+ "branch": {
+ Type: "string",
+ Description: "Filter workflow runs to a specific Git branch. Use the name of the branch.",
+ },
+ "event": {
+ Type: "string",
+ Description: "Filter workflow runs to 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: "Filter workflow runs to only runs with a specific status",
+ Enum: []any{"queued", "in_progress", "completed", "requested", "waiting"},
+ },
+ },
},
- "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",
+ "workflow_jobs_filter": {
+ Type: "object",
+ Description: "Filters for workflow jobs. **ONLY** used when method is 'list_workflow_jobs'",
+ Properties: map[string]*jsonschema.Schema{
+ "filter": {
+ Type: "string",
+ Description: "Filters jobs by their completed_at timestamp",
+ Enum: []any{"latest", "all"},
+ },
},
},
- "status": {
- Type: "string",
- Description: "Returns workflow runs with the check run status",
- Enum: []any{"queued", "in_progress", "completed", "requested", "waiting"},
+ "page": {
+ Type: "number",
+ Description: "Page number for pagination (default: 1)",
+ Minimum: jsonschema.Ptr(1.0),
+ },
+ "per_page": {
+ Type: "number",
+ Description: "Results per page for pagination (default: 30, max: 100)",
+ Minimum: jsonschema.Ptr(1.0),
+ Maximum: jsonschema.Ptr(100.0),
},
},
- Required: []string{"owner", "repo", "workflow_id"},
- }),
+ Required: []string{"method", "owner", "repo"},
+ },
},
func(ctx context.Context, _ *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
}
+
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
- }
- // 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")
+ method, err := RequiredParam[string](args, "method")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
- status, err := OptionalParam[string](args, "status")
+
+ resourceID, err := OptionalParam[string](args, "resource_id")
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
@@ -211,67 +192,86 @@ func ListWorkflowRuns(getClient GetClientFn, t translations.TranslationHelperFun
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
- // Set up list options
- opts := &github.ListWorkflowRunsOptions{
- Actor: actor,
- Branch: branch,
- Event: event,
- Status: status,
- ListOptions: github.ListOptions{
- PerPage: pagination.PerPage,
- Page: pagination.Page,
- },
- }
+ var resourceIDInt int64
+ var parseErr error
+ switch method {
+ case actionsMethodListWorkflows:
+ // Do nothing, no resource ID needed
+ default:
+ if resourceID == "" {
+ return utils.NewToolResultError(fmt.Sprintf("missing required parameter for method %s: resource_id", method)), nil, nil
+ }
- 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)
+ // For list_workflow_runs, resource_id could be a filename or numeric ID
+ // For other actions, resource ID must be an integer
+ if method != actionsMethodListWorkflowRuns {
+ resourceIDInt, parseErr = strconv.ParseInt(resourceID, 10, 64)
+ if parseErr != nil {
+ return utils.NewToolResultError(fmt.Sprintf("invalid resource_id, must be an integer for method %s: %v", method, parseErr)), nil, nil
+ }
+ }
}
- defer func() { _ = resp.Body.Close() }()
- r, err := json.Marshal(workflowRuns)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
+ switch method {
+ case actionsMethodListWorkflows:
+ return listWorkflows(ctx, client, args, owner, repo, pagination)
+ case actionsMethodListWorkflowRuns:
+ return listWorkflowRuns(ctx, client, args, owner, repo, resourceID, pagination)
+ case actionsMethodListWorkflowJobs:
+ return listWorkflowJobs(ctx, client, args, owner, repo, resourceIDInt, pagination)
+ case actionsMethodListWorkflowArtifacts:
+ return listWorkflowArtifacts(ctx, client, args, owner, repo, resourceIDInt, pagination)
+ default:
+ return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
}
-
- return utils.NewToolResultText(string(r)), nil, nil
}
}
-// RunWorkflow creates a tool to run an Actions workflow
-func RunWorkflow(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
+// ActionsGet returns the tool and handler for getting GitHub Actions resources.
+func ActionsGet(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
return mcp.Tool{
- Name: "run_workflow",
- Description: t("TOOL_RUN_WORKFLOW_DESCRIPTION", "Run an Actions workflow by workflow ID or filename"),
+ Name: "actions_get",
+ Description: t("TOOL_ACTIONS_GET_DESCRIPTION", `Get details about specific GitHub Actions resources.
+Use this tool to get details about individual workflows, workflow runs, jobs, and artifacts by their unique IDs.
+`),
Annotations: &mcp.ToolAnnotations{
- Title: t("TOOL_RUN_WORKFLOW_USER_TITLE", "Run workflow"),
- ReadOnlyHint: false,
+ Title: t("TOOL_ACTIONS_GET_USER_TITLE", "Get details of GitHub Actions resources (workflows, workflow runs, jobs, and artifacts)"),
+ ReadOnlyHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
- "owner": {
- Type: "string",
- Description: DescriptionRepositoryOwner,
- },
- "repo": {
+ "method": {
Type: "string",
- Description: DescriptionRepositoryName,
+ Description: "The method to execute",
+ Enum: []any{
+ actionsMethodGetWorkflow,
+ actionsMethodGetWorkflowRun,
+ actionsMethodGetWorkflowJob,
+ actionsMethodDownloadWorkflowArtifact,
+ actionsMethodGetWorkflowRunUsage,
+ actionsMethodGetWorkflowRunLogsURL,
+ },
},
- "workflow_id": {
+ "owner": {
Type: "string",
- Description: "The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml)",
+ Description: "Repository owner",
},
- "ref": {
+ "repo": {
Type: "string",
- Description: "The git reference for the workflow. The reference can be a branch or tag name.",
+ Description: "Repository name",
},
- "inputs": {
- Type: "object",
- Description: "Inputs the workflow accepts",
+ "resource_id": {
+ Type: "string",
+ Description: `The unique identifier of the resource. This will vary based on the "method" provided, so ensure you provide the correct ID:
+- Provide a workflow ID or workflow file name (e.g. ci.yaml) for 'get_workflow' method.
+- Provide a workflow run ID for 'get_workflow_run', 'get_workflow_run_usage', and 'get_workflow_run_logs_url' methods.
+- Provide an artifact ID for 'download_workflow_run_artifact' method.
+- Provide a job ID for 'get_workflow_job' method.
+`,
},
},
- Required: []string{"owner", "repo", "workflow_id", "ref"},
+ Required: []string{"method", "owner", "repo", "resource_id"},
},
},
func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
@@ -283,236 +283,104 @@ func RunWorkflow(getClient GetClientFn, t translations.TranslationHelperFunc) (m
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
- workflowID, err := RequiredParam[string](args, "workflow_id")
+ method, err := RequiredParam[string](args, "method")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
- ref, err := RequiredParam[string](args, "ref")
+
+ resourceID, err := RequiredParam[string](args, "resource_id")
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
- }
- }
-
client, err := getClient(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
- 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,
+ var resourceIDInt int64
+ var parseErr error
+ switch method {
+ case actionsMethodGetWorkflow:
+ // Do nothing, we accept both a string workflow ID or filename
+ default:
+ // For other methods, resource ID must be an integer
+ resourceIDInt, parseErr = strconv.ParseInt(resourceID, 10, 64)
+ if parseErr != nil {
+ return utils.NewToolResultError(fmt.Sprintf("invalid resource_id, must be an integer for method %s: %v", method, parseErr)), nil, nil
+ }
}
- r, err := json.Marshal(result)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
+ switch method {
+ case actionsMethodGetWorkflow:
+ return getWorkflow(ctx, client, args, owner, repo, resourceID)
+ case actionsMethodGetWorkflowRun:
+ return getWorkflowRun(ctx, client, args, owner, repo, resourceIDInt)
+ case actionsMethodGetWorkflowJob:
+ return getWorkflowJob(ctx, client, args, owner, repo, resourceIDInt)
+ case actionsMethodDownloadWorkflowArtifact:
+ return downloadWorkflowArtifact(ctx, client, args, owner, repo, resourceIDInt)
+ case actionsMethodGetWorkflowRunUsage:
+ return getWorkflowRunUsage(ctx, client, args, owner, repo, resourceIDInt)
+ case actionsMethodGetWorkflowRunLogsURL:
+ return getWorkflowRunLogs(ctx, client, args, owner, repo, resourceIDInt)
+ default:
+ return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
}
-
- return utils.NewToolResultText(string(r)), nil, nil
}
}
-// GetWorkflowRun creates a tool to get details of a specific workflow run
-func GetWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
+// ActionsRunTrigger returns the tool and handler for triggering GitHub Actions workflows.
+func ActionsRunTrigger(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
return mcp.Tool{
- Name: "get_workflow_run",
- Description: t("TOOL_GET_WORKFLOW_RUN_DESCRIPTION", "Get details of a specific workflow run"),
+ Name: "actions_run_trigger",
+ Description: t("TOOL_ACTIONS_RUN_TRIGGER_DESCRIPTION", "Trigger GitHub Actions workflow operations, including running, re-running, cancelling workflow runs, and deleting workflow run logs."),
Annotations: &mcp.ToolAnnotations{
- Title: t("TOOL_GET_WORKFLOW_RUN_USER_TITLE", "Get workflow run"),
- ReadOnlyHint: true,
+ Title: t("TOOL_ACTIONS_RUN_TRIGGER_USER_TITLE", "Trigger GitHub Actions workflow actions"),
+ ReadOnlyHint: false,
+ DestructiveHint: jsonschema.Ptr(true),
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
- "owner": {
+ "method": {
Type: "string",
- Description: DescriptionRepositoryOwner,
- },
- "repo": {
- Type: "string",
- Description: DescriptionRepositoryName,
- },
- "run_id": {
- Type: "number",
- Description: "The unique identifier of the workflow run",
+ Description: "The method to execute",
+ Enum: []any{
+ actionsMethodRunWorkflow,
+ actionsMethodRerunWorkflowRun,
+ actionsMethodRerunFailedJobs,
+ actionsMethodCancelWorkflowRun,
+ actionsMethodDeleteWorkflowRunLogs,
+ },
},
- },
- Required: []string{"owner", "repo", "run_id"},
- },
- },
- func(ctx context.Context, _ *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
- }
- 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)
-
- client, err := getClient(ctx)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
- }
-
- 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
- }
-}
-
-// GetWorkflowRunLogs creates a tool to download logs for a specific workflow run
-func GetWorkflowRunLogs(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
- return 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,
+ Description: "Repository owner",
},
"repo": {
Type: "string",
- Description: DescriptionRepositoryName,
- },
- "run_id": {
- Type: "number",
- Description: "The unique identifier of the workflow run",
+ Description: "Repository name",
},
- },
- Required: []string{"owner", "repo", "run_id"},
- },
- },
- func(ctx context.Context, _ *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
- }
- 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)
-
- client, err := getClient(ctx)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
- }
-
- // 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
- }
-}
-
-// ListWorkflowJobs creates a tool to list jobs for a specific workflow run
-func ListWorkflowJobs(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
- return 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": {
+ "workflow_id": {
Type: "string",
- Description: DescriptionRepositoryOwner,
+ Description: "The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml). Required for 'run_workflow' method.",
},
- "repo": {
+ "ref": {
Type: "string",
- Description: DescriptionRepositoryName,
+ Description: "The git reference for the workflow. The reference can be a branch or tag name. Required for 'run_workflow' method.",
+ },
+ "inputs": {
+ Type: "object",
+ Description: "Inputs the workflow accepts. Only used for 'run_workflow' method.",
},
"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"},
+ Description: "The ID of the workflow run. Required for all methods except 'run_workflow'.",
},
},
- Required: []string{"owner", "repo", "run_id"},
- }),
+ Required: []string{"method", "owner", "repo"},
+ },
},
func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
owner, err := RequiredParam[string](args, "owner")
@@ -523,22 +391,34 @@ func ListWorkflowJobs(getClient GetClientFn, t translations.TranslationHelperFun
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
- runIDInt, err := RequiredInt(args, "run_id")
+ method, err := RequiredParam[string](args, "method")
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 parameters
+ workflowID, _ := OptionalParam[string](args, "workflow_id")
+ ref, _ := OptionalParam[string](args, "ref")
+ runID, _ := OptionalIntParam(args, "run_id")
+
+ // 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
+ }
}
- // Get optional pagination parameters
- pagination, err := OptionalPaginationParams(args)
- if err != nil {
- return utils.NewToolResultError(err.Error()), nil, nil
+ // Validate required parameters based on action type
+ if method == actionsMethodRunWorkflow {
+ if workflowID == "" {
+ return utils.NewToolResultError("workflow_id is required for run_workflow action"), nil, nil
+ }
+ if ref == "" {
+ return utils.NewToolResultError("ref is required for run_workflow action"), nil, nil
+ }
+ } else if runID == 0 {
+ return utils.NewToolResultError("missing required parameter: run_id"), nil, nil
}
client, err := getClient(ctx)
@@ -546,43 +426,33 @@ func ListWorkflowJobs(getClient GetClientFn, t translations.TranslationHelperFun
return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
}
- // 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",
+ switch method {
+ case actionsMethodRunWorkflow:
+ return runWorkflow(ctx, client, args, owner, repo, workflowID, ref, inputs)
+ case actionsMethodRerunWorkflowRun:
+ return rerunWorkflowRun(ctx, client, args, owner, repo, int64(runID))
+ case actionsMethodRerunFailedJobs:
+ return rerunFailedJobs(ctx, client, args, owner, repo, int64(runID))
+ case actionsMethodCancelWorkflowRun:
+ return cancelWorkflowRun(ctx, client, args, owner, repo, int64(runID))
+ case actionsMethodDeleteWorkflowRunLogs:
+ return deleteWorkflowRunLogs(ctx, client, args, owner, repo, int64(runID))
+ default:
+ return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
}
-
- 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
}
}
-// 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(getClient GetClientFn, t translations.TranslationHelperFunc, contentWindowSize int) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
+// ActionsGetJobLogs returns the tool and handler for getting workflow job logs.
+func ActionsGetJobLogs(getClient GetClientFn, t translations.TranslationHelperFunc, contentWindowSize int) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
return 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"),
+ Name: "get_job_logs",
+ Description: t("TOOL_GET_JOB_LOGS_DESCRIPTION", `Get logs for GitHub Actions workflow jobs.
+Use this tool to retrieve logs for a specific job or all failed jobs in a workflow run.
+For single job logs, provide job_id. For all failed jobs in a run, provide run_id with failed_only=true.
+`),
Annotations: &mcp.ToolAnnotations{
- Title: t("TOOL_GET_JOB_LOGS_USER_TITLE", "Get job logs"),
+ Title: t("TOOL_GET_JOB_LOGS_USER_TITLE", "Get GitHub Actions workflow job logs"),
ReadOnlyHint: true,
},
InputSchema: &jsonschema.Schema{
@@ -590,23 +460,23 @@ func GetJobLogs(getClient GetClientFn, t translations.TranslationHelperFunc, con
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
- Description: DescriptionRepositoryOwner,
+ Description: "Repository owner",
},
"repo": {
Type: "string",
- Description: DescriptionRepositoryName,
+ Description: "Repository name",
},
"job_id": {
Type: "number",
- Description: "The unique identifier of the workflow job (required for single job logs)",
+ Description: "The unique identifier of the workflow job. Required when getting logs for a single job.",
},
"run_id": {
Type: "number",
- Description: "Workflow run ID (required when using failed_only)",
+ Description: "The unique identifier of the workflow run. Required when failed_only is true to get logs for all failed jobs in the run.",
},
"failed_only": {
Type: "boolean",
- Description: "When true, gets logs for all failed jobs in run_id",
+ Description: "When true, gets logs for all failed jobs in the workflow run specified by run_id. Requires run_id to be provided.",
},
"return_content": {
Type: "boolean",
@@ -631,23 +501,26 @@ func GetJobLogs(getClient GetClientFn, t translations.TranslationHelperFunc, con
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
@@ -682,18 +555,393 @@ func GetJobLogs(getClient GetClientFn, t translations.TranslationHelperFunc, con
}
}
-// 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",
- })
+// Helper functions for consolidated actions tools
+
+func getWorkflow(ctx context.Context, client *github.Client, _ map[string]any, owner, repo, resourceID string) (*mcp.CallToolResult, any, error) {
+ var workflow *github.Workflow
+ var resp *github.Response
+ var err error
+
+ if workflowIDInt, parseErr := strconv.ParseInt(resourceID, 10, 64); parseErr == nil {
+ workflow, resp, err = client.Actions.GetWorkflowByID(ctx, owner, repo, workflowIDInt)
+ } else {
+ workflow, resp, err = client.Actions.GetWorkflowByFileName(ctx, owner, repo, resourceID)
+ }
+
if err != nil {
- return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow jobs", resp, err), nil, nil
+ return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get workflow", resp, err), nil, nil
}
+
defer func() { _ = resp.Body.Close() }()
+ r, err := json.Marshal(workflow)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal workflow: %w", err)
+ }
- // Filter for failed jobs
+ return utils.NewToolResultText(string(r)), nil, nil
+}
+
+func getWorkflowRun(ctx context.Context, client *github.Client, _ map[string]any, owner, repo string, resourceID int64) (*mcp.CallToolResult, any, error) {
+ workflowRun, resp, err := client.Actions.GetWorkflowRunByID(ctx, owner, repo, resourceID)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get workflow run", resp, err), nil, nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+ r, err := json.Marshal(workflowRun)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal workflow run: %w", err)
+ }
+ return utils.NewToolResultText(string(r)), nil, nil
+}
+
+func getWorkflowJob(ctx context.Context, client *github.Client, _ map[string]any, owner, repo string, resourceID int64) (*mcp.CallToolResult, any, error) {
+ workflowJob, resp, err := client.Actions.GetWorkflowJobByID(ctx, owner, repo, resourceID)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get workflow job", resp, err), nil, nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+ r, err := json.Marshal(workflowJob)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal workflow job: %w", err)
+ }
+ return utils.NewToolResultText(string(r)), nil, nil
+}
+
+func listWorkflows(ctx context.Context, client *github.Client, _ map[string]any, owner, repo string, pagination PaginationParams) (*mcp.CallToolResult, any, error) {
+ opts := &github.ListOptions{
+ PerPage: pagination.PerPage,
+ Page: pagination.Page,
+ }
+
+ workflows, resp, err := client.Actions.ListWorkflows(ctx, owner, repo, opts)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflows", resp, err), nil, nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ r, err := json.Marshal(workflows)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal workflows: %w", err)
+ }
+
+ return utils.NewToolResultText(string(r)), nil, nil
+}
+
+func listWorkflowRuns(ctx context.Context, client *github.Client, args map[string]any, owner, repo, resourceID string, pagination PaginationParams) (*mcp.CallToolResult, any, error) {
+ filterArgs, err := OptionalParam[map[string]any](args, "workflow_runs_filter")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ filterArgsTyped := make(map[string]string)
+ for k, v := range filterArgs {
+ if strVal, ok := v.(string); ok {
+ filterArgsTyped[k] = strVal
+ } else {
+ filterArgsTyped[k] = ""
+ }
+ }
+
+ listWorkflowRunsOptions := &github.ListWorkflowRunsOptions{
+ Actor: filterArgsTyped["actor"],
+ Branch: filterArgsTyped["branch"],
+ Event: filterArgsTyped["event"],
+ Status: filterArgsTyped["status"],
+ ListOptions: github.ListOptions{
+ Page: pagination.Page,
+ PerPage: pagination.PerPage,
+ },
+ }
+
+ var workflowRuns *github.WorkflowRuns
+ var resp *github.Response
+
+ if workflowIDInt, parseErr := strconv.ParseInt(resourceID, 10, 64); parseErr == nil {
+ workflowRuns, resp, err = client.Actions.ListWorkflowRunsByID(ctx, owner, repo, workflowIDInt, listWorkflowRunsOptions)
+ } else {
+ workflowRuns, resp, err = client.Actions.ListWorkflowRunsByFileName(ctx, owner, repo, resourceID, listWorkflowRunsOptions)
+ }
+
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow runs", resp, err), nil, nil
+ }
+
+ defer func() { _ = resp.Body.Close() }()
+ r, err := json.Marshal(workflowRuns)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal workflow runs: %w", err)
+ }
+
+ return utils.NewToolResultText(string(r)), nil, nil
+}
+
+func listWorkflowJobs(ctx context.Context, client *github.Client, args map[string]any, owner, repo string, resourceID int64, pagination PaginationParams) (*mcp.CallToolResult, any, error) {
+ filterArgs, err := OptionalParam[map[string]any](args, "workflow_jobs_filter")
+ if err != nil {
+ return utils.NewToolResultError(err.Error()), nil, nil
+ }
+
+ filterArgsTyped := make(map[string]string)
+ for k, v := range filterArgs {
+ if strVal, ok := v.(string); ok {
+ filterArgsTyped[k] = strVal
+ } else {
+ filterArgsTyped[k] = ""
+ }
+ }
+
+ workflowJobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, resourceID, &github.ListWorkflowJobsOptions{
+ Filter: filterArgsTyped["filter"],
+ ListOptions: github.ListOptions{
+ Page: pagination.Page,
+ PerPage: pagination.PerPage,
+ },
+ })
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow jobs", resp, err), nil, nil
+ }
+
+ response := map[string]any{
+ "jobs": workflowJobs,
+ }
+
+ defer func() { _ = resp.Body.Close() }()
+ r, err := json.Marshal(response)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal workflow jobs: %w", err)
+ }
+
+ return utils.NewToolResultText(string(r)), nil, nil
+}
+
+func listWorkflowArtifacts(ctx context.Context, client *github.Client, _ map[string]any, owner, repo string, resourceID int64, pagination PaginationParams) (*mcp.CallToolResult, any, error) {
+ opts := &github.ListOptions{
+ PerPage: pagination.PerPage,
+ Page: pagination.Page,
+ }
+
+ artifacts, resp, err := client.Actions.ListWorkflowRunArtifacts(ctx, owner, repo, resourceID, 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
+}
+
+func downloadWorkflowArtifact(ctx context.Context, client *github.Client, _ map[string]any, owner, repo string, resourceID int64) (*mcp.CallToolResult, any, error) {
+ // Get the download URL for the artifact
+ url, resp, err := client.Actions.DownloadArtifact(ctx, owner, repo, resourceID, 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": resourceID,
+ }
+
+ 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
+}
+
+func getWorkflowRunLogs(ctx context.Context, client *github.Client, _ map[string]any, owner, repo string, runID int64) (*mcp.CallToolResult, any, error) {
+ // Get the download URL for the logs
+ url, resp, err := client.Actions.GetWorkflowRunLogs(ctx, owner, repo, runID, 1)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get workflow run logs", resp, err), nil, nil
+ }
+ 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
+}
+
+func getWorkflowRunUsage(ctx context.Context, client *github.Client, _ map[string]any, owner, repo string, resourceID int64) (*mcp.CallToolResult, any, error) {
+ usage, resp, err := client.Actions.GetWorkflowRunUsageByID(ctx, owner, repo, resourceID)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get workflow run usage", resp, err), nil, nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ r, err := json.Marshal(usage)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return utils.NewToolResultText(string(r)), nil, nil
+}
+
+func runWorkflow(ctx context.Context, client *github.Client, _ map[string]any, owner, repo, workflowID, ref string, inputs map[string]interface{}) (*mcp.CallToolResult, any, error) {
+ event := github.CreateWorkflowDispatchEventRequest{
+ Ref: ref,
+ Inputs: inputs,
+ }
+
+ var resp *github.Response
+ var err error
+ 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 ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to run workflow", resp, err), nil, nil
+ }
+ 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
+}
+
+func rerunWorkflowRun(ctx context.Context, client *github.Client, _ map[string]any, owner, repo string, runID int64) (*mcp.CallToolResult, any, error) {
+ 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
+}
+
+func rerunFailedJobs(ctx context.Context, client *github.Client, _ map[string]any, owner, repo string, runID int64) (*mcp.CallToolResult, any, error) {
+ 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
+}
+
+func cancelWorkflowRun(ctx context.Context, client *github.Client, _ map[string]any, owner, repo string, runID int64) (*mcp.CallToolResult, any, error) {
+ resp, err := client.Actions.CancelWorkflowRunByID(ctx, owner, repo, runID)
+ if err != nil {
+ var acceptedErr *github.AcceptedError
+ if !errors.As(err, &acceptedErr) {
+ 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
+}
+
+func deleteWorkflowRunLogs(ctx context.Context, client *github.Client, _ map[string]any, owner, repo string, runID int64) (*mcp.CallToolResult, any, error) {
+ 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)
+ }
+
+ return utils.NewToolResultText(string(r)), 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, 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" {
@@ -748,7 +996,7 @@ func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo
}
// 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) {
+func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo string, jobID int64, returnContent bool, tailLines, 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
@@ -763,7 +1011,7 @@ func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo
}
// 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) {
+func getJobLogData(ctx context.Context, client *github.Client, owner, repo string, jobID int64, jobName string, returnContent bool, tailLines, 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 {
@@ -801,7 +1049,7 @@ func getJobLogData(ctx context.Context, client *github.Client, owner, repo strin
return result, resp, nil
}
-func downloadLogContent(ctx context.Context, logURL string, tailLines int, maxLines int) (string, int, *http.Response, error) {
+func downloadLogContent(ctx context.Context, logURL string, tailLines, maxLines int) (string, int, *http.Response, error) {
prof := profiler.New(nil, profiler.IsProfilingEnabled())
finish := prof.Start(ctx, "log_buffer_processing")
@@ -835,496 +1083,3 @@ func downloadLogContent(ctx context.Context, logURL string, tailLines int, maxLi
return finalResult, totalLines, httpResp, nil
}
-
-// RerunWorkflowRun creates a tool to re-run an entire workflow run
-func RerunWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
- return 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"},
- },
- },
- func(ctx context.Context, _ *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
- }
- 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)
-
- client, err := getClient(ctx)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
- }
-
- 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
- }
-}
-
-// RerunFailedJobs creates a tool to re-run only the failed jobs in a workflow run
-func RerunFailedJobs(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
- return 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"},
- },
- },
- func(ctx context.Context, _ *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
- }
- 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)
-
- client, err := getClient(ctx)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
- }
-
- 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
- }
-}
-
-// CancelWorkflowRun creates a tool to cancel a workflow run
-func CancelWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
- return 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"},
- },
- },
- func(ctx context.Context, _ *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
- }
- 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)
-
- client, err := getClient(ctx)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
- }
-
- 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
- }
-}
-
-// ListWorkflowRunArtifacts creates a tool to list artifacts for a workflow run
-func ListWorkflowRunArtifacts(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
- return 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"},
- }),
- },
- func(ctx context.Context, _ *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
- }
- 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
- }
-
- client, err := getClient(ctx)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
- }
-
- // 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
- }
-}
-
-// DownloadWorkflowRunArtifact creates a tool to download a workflow run artifact
-func DownloadWorkflowRunArtifact(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
- return 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"},
- },
- },
- func(ctx context.Context, _ *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
- }
- 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)
-
- client, err := getClient(ctx)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
- }
-
- // 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
- }
-}
-
-// DeleteWorkflowRunLogs creates a tool to delete logs for a workflow run
-func DeleteWorkflowRunLogs(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
- return 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"},
- },
- },
- func(ctx context.Context, _ *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
- }
- 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)
-
- client, err := getClient(ctx)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
- }
-
- 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)
- }
-
- return utils.NewToolResultText(string(r)), nil, nil
- }
-}
-
-// GetWorkflowRunUsage creates a tool to get usage metrics for a workflow run
-func GetWorkflowRunUsage(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
- return 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"},
- },
- },
- func(ctx context.Context, _ *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
- }
- 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)
-
- client, err := getClient(ctx)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to get GitHub client: %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() }()
-
- r, err := json.Marshal(usage)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
- }
-
- return utils.NewToolResultText(string(r)), nil, nil
- }
-}
diff --git a/pkg/github/actions_test.go b/pkg/github/actions_test.go
index 6d9921f2e..397dfc81d 100644
--- a/pkg/github/actions_test.go
+++ b/pkg/github/actions_test.go
@@ -23,21 +23,25 @@ import (
"github.com/stretchr/testify/require"
)
-func Test_ListWorkflows(t *testing.T) {
+func Test_ActionsList(t *testing.T) {
// Verify tool definition once
mockClient := github.NewClient(nil)
- tool, _ := ListWorkflows(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+ tool, _ := ActionsList(stubGetClientFn(mockClient), translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(tool.Name, tool))
- assert.Equal(t, "list_workflows", tool.Name)
+ assert.Equal(t, "actions_list", tool.Name)
assert.NotEmpty(t, tool.Description)
inputSchema := tool.InputSchema.(*jsonschema.Schema)
+ assert.Contains(t, inputSchema.Properties, "method")
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"})
+ assert.Contains(t, inputSchema.Properties, "resource_id")
+ assert.Contains(t, inputSchema.Properties, "workflow_runs_filter")
+ assert.Contains(t, inputSchema.Properties, "workflow_jobs_filter")
+ assert.ElementsMatch(t, inputSchema.Required, []string{"method", "owner", "repo"})
+}
+func Test_ActionsList_ListWorkflows(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
@@ -86,8 +90,9 @@ func Test_ListWorkflows(t *testing.T) {
),
),
requestArgs: map[string]any{
- "owner": "owner",
- "repo": "repo",
+ "method": "list_workflows",
+ "owner": "owner",
+ "repo": "repo",
},
expectError: false,
},
@@ -95,7 +100,8 @@ func Test_ListWorkflows(t *testing.T) {
name: "missing required parameter owner",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
- "repo": "repo",
+ "method": "list_workflows",
+ "repo": "repo",
},
expectError: true,
expectedErrMsg: "missing required parameter: owner",
@@ -104,20 +110,14 @@ func Test_ListWorkflows(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- // Setup client with mock
client := github.NewClient(tc.mockedClient)
- _, handler := ListWorkflows(stubGetClientFn(client), translations.NullTranslationHelper)
-
- // Create call request
+ _, handler := ActionsList(stubGetClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
-
- // Call handler
result, _, err := handler(context.Background(), &request, tc.requestArgs)
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 != "" {
@@ -125,7 +125,6 @@ func Test_ListWorkflows(t *testing.T) {
return
}
- // Unmarshal and verify the result
var response github.Workflows
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
@@ -136,21 +135,7 @@ func Test_ListWorkflows(t *testing.T) {
}
}
-func Test_RunWorkflow(t *testing.T) {
- // Verify tool definition once
- mockClient := github.NewClient(nil)
- tool, _ := RunWorkflow(stubGetClientFn(mockClient), translations.NullTranslationHelper)
- require.NoError(t, toolsnaps.Test(tool.Name, tool))
-
- assert.Equal(t, "run_workflow", tool.Name)
- assert.NotEmpty(t, tool.Description)
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "workflow_id")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "ref")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "inputs")
- assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "workflow_id", "ref"})
-
+func Test_ActionsList_ListWorkflowRuns(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
@@ -159,52 +144,57 @@ func Test_RunWorkflow(t *testing.T) {
expectedErrMsg string
}{
{
- name: "successful workflow run",
+ name: "successful workflow runs listing",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
- mock.PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowId,
+ mock.GetReposActionsWorkflowsRunsByOwnerByRepoByWorkflowId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- w.WriteHeader(http.StatusNoContent)
+ workflowRuns := &github.WorkflowRuns{
+ TotalCount: github.Ptr(1),
+ WorkflowRuns: []*github.WorkflowRun{
+ {
+ ID: github.Ptr(int64(123)),
+ Name: github.Ptr("CI"),
+ Status: github.Ptr("completed"),
+ },
+ },
+ }
+ w.WriteHeader(http.StatusOK)
+ _ = json.NewEncoder(w).Encode(workflowRuns)
}),
),
),
requestArgs: map[string]any{
+ "method": "list_workflow_runs",
"owner": "owner",
"repo": "repo",
- "workflow_id": "12345",
- "ref": "main",
+ "resource_id": "ci.yml",
},
expectError: false,
},
{
- name: "missing required parameter workflow_id",
+ name: "missing resource_id for list_workflow_runs",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
- "owner": "owner",
- "repo": "repo",
- "ref": "main",
+ "method": "list_workflow_runs",
+ "owner": "owner",
+ "repo": "repo",
},
expectError: true,
- expectedErrMsg: "missing required parameter: workflow_id",
+ expectedErrMsg: "missing required parameter for method list_workflow_runs: resource_id",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- // Setup client with mock
client := github.NewClient(tc.mockedClient)
- _, handler := RunWorkflow(stubGetClientFn(client), translations.NullTranslationHelper)
-
- // Create call request
+ _, handler := ActionsList(stubGetClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
-
- // Call handler
result, _, err := handler(context.Background(), &request, tc.requestArgs)
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 != "" {
@@ -212,18 +202,15 @@ func Test_RunWorkflow(t *testing.T) {
return
}
- // Unmarshal and verify the result
- var response map[string]any
+ var response github.WorkflowRuns
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")
+ assert.NotNil(t, response.TotalCount)
})
}
}
-func Test_RunWorkflow_WithFilename(t *testing.T) {
- // Test the unified RunWorkflow function with filenames
+func Test_ActionsList_ListWorkflowArtifacts(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
@@ -232,70 +219,64 @@ func Test_RunWorkflow_WithFilename(t *testing.T) {
expectedErrMsg string
}{
{
- name: "successful workflow run by filename",
- mockedClient: mock.NewMockedHTTPClient(
- mock.WithRequestMatchHandler(
- mock.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",
+ name: "successful artifacts listing",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
- mock.PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowId,
+ mock.GetReposActionsRunsArtifactsByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- w.WriteHeader(http.StatusNoContent)
+ 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{},
+ },
+ },
+ }
+ w.WriteHeader(http.StatusOK)
+ _ = json.NewEncoder(w).Encode(artifacts)
}),
),
),
requestArgs: map[string]any{
+ "method": "list_workflow_run_artifacts",
"owner": "owner",
"repo": "repo",
- "workflow_id": "12345",
- "ref": "main",
+ "resource_id": "12345",
},
expectError: false,
},
{
- name: "missing required parameter workflow_id",
+ name: "missing resource_id for list_workflow_run_artifacts",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
- "owner": "owner",
- "repo": "repo",
- "ref": "main",
+ "method": "list_workflow_run_artifacts",
+ "owner": "owner",
+ "repo": "repo",
},
expectError: true,
- expectedErrMsg: "missing required parameter: workflow_id",
+ expectedErrMsg: "missing required parameter for method list_workflow_run_artifacts: resource_id",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- // Setup client with mock
client := github.NewClient(tc.mockedClient)
- _, handler := RunWorkflow(stubGetClientFn(client), translations.NullTranslationHelper)
-
- // Create call request
+ _, handler := ActionsList(stubGetClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
-
- // Call handler
result, _, err := handler(context.Background(), &request, tc.requestArgs)
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 != "" {
@@ -303,29 +284,31 @@ func Test_RunWorkflow_WithFilename(t *testing.T) {
return
}
- // Unmarshal and verify the result
- var response map[string]any
+ var response github.ArtifactList
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")
+ assert.NotNil(t, response.TotalCount)
})
}
}
-func Test_CancelWorkflowRun(t *testing.T) {
+func Test_ActionsGet(t *testing.T) {
// Verify tool definition once
mockClient := github.NewClient(nil)
- tool, _ := CancelWorkflowRun(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+ tool, _ := ActionsGet(stubGetClientFn(mockClient), translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(tool.Name, tool))
- assert.Equal(t, "cancel_workflow_run", tool.Name)
+ assert.Equal(t, "actions_get", tool.Name)
assert.NotEmpty(t, tool.Description)
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "run_id")
- assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "run_id"})
+ inputSchema := tool.InputSchema.(*jsonschema.Schema)
+ assert.Contains(t, inputSchema.Properties, "method")
+ assert.Contains(t, inputSchema.Properties, "owner")
+ assert.Contains(t, inputSchema.Properties, "repo")
+ assert.Contains(t, inputSchema.Properties, "resource_id")
+ assert.ElementsMatch(t, inputSchema.Required, []string{"method", "owner", "repo", "resource_id"})
+}
+func Test_ActionsGet_GetWorkflowRun(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
@@ -334,106 +317,58 @@ func Test_CancelWorkflowRun(t *testing.T) {
expectedErrMsg string
}{
{
- name: "successful workflow run cancellation",
+ name: "successful get workflow run",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
- mock.EndpointPattern{
- Pattern: "/repos/owner/repo/actions/runs/12345/cancel",
- Method: "POST",
- },
+ mock.GetReposActionsRunsByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- w.WriteHeader(http.StatusAccepted)
+ workflowRun := &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(workflowRun)
}),
),
),
requestArgs: map[string]any{
- "owner": "owner",
- "repo": "repo",
- "run_id": float64(12345),
+ "method": "get_workflow_run",
+ "owner": "owner",
+ "repo": "repo",
+ "resource_id": "12345",
},
expectError: false,
},
- {
- name: "conflict when cancelling a workflow run",
- mockedClient: mock.NewMockedHTTPClient(
- mock.WithRequestMatchHandler(
- mock.EndpointPattern{
- Pattern: "/repos/owner/repo/actions/runs/12345/cancel",
- Method: "POST",
- },
- 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: mock.NewMockedHTTPClient(),
- 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)
- _, handler := CancelWorkflowRun(stubGetClientFn(client), translations.NullTranslationHelper)
-
- // Create call request
+ _, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
-
- // Call handler
result, _, err := handler(context.Background(), &request, tc.requestArgs)
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)
+ assert.Equal(t, tc.expectedErrMsg, textContent.Text)
return
}
- // Unmarshal and verify the result
- var response map[string]any
+ var response github.WorkflowRun
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"])
+ assert.Equal(t, int64(12345), response.GetID())
})
}
}
-func Test_ListWorkflowRunArtifacts(t *testing.T) {
- // Verify tool definition once
- mockClient := github.NewClient(nil)
- tool, _ := ListWorkflowRunArtifacts(stubGetClientFn(mockClient), translations.NullTranslationHelper)
- require.NoError(t, toolsnaps.Test(tool.Name, tool))
-
- assert.Equal(t, "list_workflow_run_artifacts", tool.Name)
- assert.NotEmpty(t, tool.Description)
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "run_id")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "perPage")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "page")
- assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "run_id"})
-
+func Test_ActionsGet_DownloadArtifact(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
@@ -442,94 +377,50 @@ func Test_ListWorkflowRunArtifacts(t *testing.T) {
expectedErrMsg string
}{
{
- name: "successful artifacts listing",
+ name: "successful artifact download URL",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
- mock.GetReposActionsRunsArtifactsByOwnerByRepoByRunId,
+ mock.EndpointPattern{
+ Pattern: "/repos/owner/repo/actions/artifacts/123/zip",
+ Method: "GET",
+ },
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)
+ 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",
- "run_id": float64(12345),
+ "method": "download_workflow_run_artifact",
+ "owner": "owner",
+ "repo": "repo",
+ "resource_id": "123",
},
expectError: false,
},
{
- name: "missing required parameter run_id",
+ name: "missing required parameter resource_id",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
- "owner": "owner",
- "repo": "repo",
+ "method": "download_workflow_run_artifact",
+ "owner": "owner",
+ "repo": "repo",
},
expectError: true,
- expectedErrMsg: "missing required parameter: run_id",
+ expectedErrMsg: "missing required parameter: resource_id",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- // Setup client with mock
client := github.NewClient(tc.mockedClient)
- _, handler := ListWorkflowRunArtifacts(stubGetClientFn(client), translations.NullTranslationHelper)
-
- // Create call request
+ _, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
-
- // Call handler
result, _, err := handler(context.Background(), &request, tc.requestArgs)
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 != "" {
@@ -537,30 +428,37 @@ func Test_ListWorkflowRunArtifacts(t *testing.T) {
return
}
- // Unmarshal and verify the result
- var response github.ArtifactList
+ var response map[string]any
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)
+ 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_DownloadWorkflowRunArtifact(t *testing.T) {
+func Test_ActionsRunTrigger(t *testing.T) {
// Verify tool definition once
mockClient := github.NewClient(nil)
- tool, _ := DownloadWorkflowRunArtifact(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+ tool, _ := ActionsRunTrigger(stubGetClientFn(mockClient), translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(tool.Name, tool))
- assert.Equal(t, "download_workflow_run_artifact", tool.Name)
+ assert.Equal(t, "actions_run_trigger", tool.Name)
assert.NotEmpty(t, tool.Description)
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "artifact_id")
- assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "artifact_id"})
+ inputSchema := tool.InputSchema.(*jsonschema.Schema)
+ assert.Contains(t, inputSchema.Properties, "method")
+ assert.Contains(t, inputSchema.Properties, "owner")
+ assert.Contains(t, inputSchema.Properties, "repo")
+ assert.Contains(t, inputSchema.Properties, "workflow_id")
+ assert.Contains(t, inputSchema.Properties, "ref")
+ assert.Contains(t, inputSchema.Properties, "inputs")
+ assert.Contains(t, inputSchema.Properties, "run_id")
+ assert.ElementsMatch(t, inputSchema.Required, []string{"method", "owner", "repo"})
+}
+func Test_ActionsRunTrigger_RunWorkflow(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
@@ -569,55 +467,79 @@ func Test_DownloadWorkflowRunArtifact(t *testing.T) {
expectedErrMsg string
}{
{
- name: "successful artifact download URL",
+ name: "successful workflow run",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
- mock.EndpointPattern{
- Pattern: "/repos/owner/repo/actions/artifacts/123/zip",
- Method: "GET",
- },
+ mock.PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowId,
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)
+ w.WriteHeader(http.StatusNoContent)
}),
),
),
requestArgs: map[string]any{
+ "method": "run_workflow",
"owner": "owner",
"repo": "repo",
- "artifact_id": float64(123),
+ "workflow_id": "12345",
+ "ref": "main",
},
expectError: false,
},
{
- name: "missing required parameter artifact_id",
+ name: "successful workflow run by filename",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowId,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusNoContent)
+ }),
+ ),
+ ),
+ requestArgs: map[string]any{
+ "method": "run_workflow",
+ "owner": "owner",
+ "repo": "repo",
+ "workflow_id": "ci.yml",
+ "ref": "main",
+ },
+ expectError: false,
+ },
+ {
+ name: "missing required parameter workflow_id",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
- "owner": "owner",
- "repo": "repo",
+ "method": "run_workflow",
+ "owner": "owner",
+ "repo": "repo",
+ "ref": "main",
+ },
+ expectError: true,
+ expectedErrMsg: "workflow_id is required for run_workflow action",
+ },
+ {
+ name: "missing required parameter ref",
+ mockedClient: mock.NewMockedHTTPClient(),
+ requestArgs: map[string]any{
+ "method": "run_workflow",
+ "owner": "owner",
+ "repo": "repo",
+ "workflow_id": "12345",
},
expectError: true,
- expectedErrMsg: "missing required parameter: artifact_id",
+ expectedErrMsg: "ref is required for run_workflow action",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- // Setup client with mock
client := github.NewClient(tc.mockedClient)
- _, handler := DownloadWorkflowRunArtifact(stubGetClientFn(client), translations.NullTranslationHelper)
-
- // Create call request
+ _, handler := ActionsRunTrigger(stubGetClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
-
- // Call handler
result, _, err := handler(context.Background(), &request, tc.requestArgs)
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 != "" {
@@ -625,31 +547,16 @@ func Test_DownloadWorkflowRunArtifact(t *testing.T) {
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"])
+ assert.Equal(t, "Workflow run has been queued", response["message"])
+ assert.Contains(t, response, "workflow_type")
})
}
}
-func Test_DeleteWorkflowRunLogs(t *testing.T) {
- // Verify tool definition once
- mockClient := github.NewClient(nil)
- tool, _ := DeleteWorkflowRunLogs(stubGetClientFn(mockClient), translations.NullTranslationHelper)
- require.NoError(t, toolsnaps.Test(tool.Name, tool))
-
- assert.Equal(t, "delete_workflow_run_logs", tool.Name)
- assert.NotEmpty(t, tool.Description)
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "run_id")
- assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "run_id"})
-
+func Test_ActionsRunTrigger_CancelWorkflowRun(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
@@ -658,28 +565,55 @@ func Test_DeleteWorkflowRunLogs(t *testing.T) {
expectedErrMsg string
}{
{
- name: "successful logs deletion",
+ name: "successful workflow run cancellation",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
- mock.DeleteReposActionsRunsLogsByOwnerByRepoByRunId,
+ mock.EndpointPattern{
+ Pattern: "/repos/owner/repo/actions/runs/12345/cancel",
+ Method: "POST",
+ },
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- w.WriteHeader(http.StatusNoContent)
+ w.WriteHeader(http.StatusAccepted)
}),
),
),
requestArgs: map[string]any{
+ "method": "cancel_workflow_run",
"owner": "owner",
"repo": "repo",
"run_id": float64(12345),
},
expectError: false,
},
+ {
+ name: "conflict when cancelling a workflow run",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.EndpointPattern{
+ Pattern: "/repos/owner/repo/actions/runs/12345/cancel",
+ Method: "POST",
+ },
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusConflict)
+ }),
+ ),
+ ),
+ requestArgs: map[string]any{
+ "method": "cancel_workflow_run",
+ "owner": "owner",
+ "repo": "repo",
+ "run_id": float64(12345),
+ },
+ expectError: true,
+ expectedErrMsg: "failed to cancel workflow run",
+ },
{
name: "missing required parameter run_id",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
- "owner": "owner",
- "repo": "repo",
+ "method": "cancel_workflow_run",
+ "owner": "owner",
+ "repo": "repo",
},
expectError: true,
expectedErrMsg: "missing required parameter: run_id",
@@ -688,50 +622,31 @@ func Test_DeleteWorkflowRunLogs(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- // Setup client with mock
client := github.NewClient(tc.mockedClient)
- _, handler := DeleteWorkflowRunLogs(stubGetClientFn(client), translations.NullTranslationHelper)
-
- // Create call request
+ _, handler := ActionsRunTrigger(stubGetClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
-
- // Call handler
result, _, err := handler(context.Background(), &request, tc.requestArgs)
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)
+ 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 logs have been deleted", response["message"])
+ assert.Equal(t, "Workflow run has been cancelled", response["message"])
assert.Equal(t, float64(12345), response["run_id"])
})
}
}
-func Test_GetWorkflowRunUsage(t *testing.T) {
- // Verify tool definition once
- mockClient := github.NewClient(nil)
- tool, _ := GetWorkflowRunUsage(stubGetClientFn(mockClient), translations.NullTranslationHelper)
- require.NoError(t, toolsnaps.Test(tool.Name, tool))
-
- assert.Equal(t, "get_workflow_run_usage", tool.Name)
- assert.NotEmpty(t, tool.Description)
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "run_id")
- assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "run_id"})
-
+func Test_ActionsRunTrigger_DeleteWorkflowRunLogs(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
@@ -740,36 +655,17 @@ func Test_GetWorkflowRunUsage(t *testing.T) {
expectedErrMsg string
}{
{
- name: "successful workflow run usage",
+ name: "successful logs deletion",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
- mock.GetReposActionsRunsTimingByOwnerByRepoByRunId,
+ mock.DeleteReposActionsRunsLogsByOwnerByRepoByRunId,
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)
+ w.WriteHeader(http.StatusNoContent)
}),
),
),
requestArgs: map[string]any{
+ "method": "delete_workflow_run_logs",
"owner": "owner",
"repo": "repo",
"run_id": float64(12345),
@@ -780,8 +676,9 @@ func Test_GetWorkflowRunUsage(t *testing.T) {
name: "missing required parameter run_id",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
- "owner": "owner",
- "repo": "repo",
+ "method": "delete_workflow_run_logs",
+ "owner": "owner",
+ "repo": "repo",
},
expectError: true,
expectedErrMsg: "missing required parameter: run_id",
@@ -790,20 +687,14 @@ func Test_GetWorkflowRunUsage(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- // Setup client with mock
client := github.NewClient(tc.mockedClient)
- _, handler := GetWorkflowRunUsage(stubGetClientFn(client), translations.NullTranslationHelper)
-
- // Create call request
+ _, handler := ActionsRunTrigger(stubGetClientFn(client), translations.NullTranslationHelper)
request := createMCPRequest(tc.requestArgs)
-
- // Call handler
result, _, err := handler(context.Background(), &request, tc.requestArgs)
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 != "" {
@@ -811,31 +702,31 @@ func Test_GetWorkflowRunUsage(t *testing.T) {
return
}
- // Unmarshal and verify the result
- var response github.WorkflowRunUsage
+ var response map[string]any
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
- assert.NotNil(t, response.RunDurationMS)
- assert.NotNil(t, response.Billable)
+ assert.Equal(t, "Workflow run logs have been deleted", response["message"])
+ assert.Equal(t, float64(12345), response["run_id"])
})
}
}
-func Test_GetJobLogs(t *testing.T) {
+func Test_ActionsGetJobLogs(t *testing.T) {
// Verify tool definition once
mockClient := github.NewClient(nil)
- tool, _ := GetJobLogs(stubGetClientFn(mockClient), translations.NullTranslationHelper, 5000)
+ tool, _ := ActionsGetJobLogs(stubGetClientFn(mockClient), translations.NullTranslationHelper, 5000)
require.NoError(t, toolsnaps.Test(tool.Name, tool))
assert.Equal(t, "get_job_logs", tool.Name)
assert.NotEmpty(t, tool.Description)
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "job_id")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "run_id")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "failed_only")
- assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "return_content")
- assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo"})
+ inputSchema := tool.InputSchema.(*jsonschema.Schema)
+ assert.Contains(t, inputSchema.Properties, "owner")
+ assert.Contains(t, inputSchema.Properties, "repo")
+ assert.Contains(t, inputSchema.Properties, "job_id")
+ assert.Contains(t, inputSchema.Properties, "run_id")
+ assert.Contains(t, inputSchema.Properties, "failed_only")
+ assert.Contains(t, inputSchema.Properties, "return_content")
+ assert.ElementsMatch(t, inputSchema.Required, []string{"owner", "repo"})
tests := []struct {
name string
@@ -1052,20 +943,14 @@ func Test_GetJobLogs(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- // Setup client with mock
client := github.NewClient(tc.mockedClient)
- _, handler := GetJobLogs(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
-
- // Create call request
+ _, handler := ActionsGetJobLogs(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
request := createMCPRequest(tc.requestArgs)
-
- // Call handler
result, _, err := handler(context.Background(), &request, tc.requestArgs)
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 != "" {
@@ -1074,12 +959,10 @@ func Test_GetJobLogs(t *testing.T) {
}
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)
@@ -1091,11 +974,9 @@ func Test_GetJobLogs(t *testing.T) {
}
}
-func Test_GetJobLogs_WithContentReturn(t *testing.T) {
- // Test the return_content functionality with a mock HTTP server
+func Test_ActionsGetJobLogs_WithContentReturn(t *testing.T) {
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))
@@ -1113,7 +994,7 @@ func Test_GetJobLogs_WithContentReturn(t *testing.T) {
)
client := github.NewClient(mockedClient)
- _, handler := GetJobLogs(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
+ _, handler := ActionsGetJobLogs(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
request := createMCPRequest(map[string]any{
"owner": "owner",
@@ -1140,15 +1021,13 @@ func Test_GetJobLogs_WithContentReturn(t *testing.T) {
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
+ assert.NotContains(t, response, "logs_url")
}
-func Test_GetJobLogs_WithContentReturnAndTailLines(t *testing.T) {
- // Test the return_content functionality with a mock HTTP server
+func Test_ActionsGetJobLogs_WithContentReturnAndTailLines(t *testing.T) {
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))
@@ -1166,14 +1045,14 @@ func Test_GetJobLogs_WithContentReturnAndTailLines(t *testing.T) {
)
client := github.NewClient(mockedClient)
- _, handler := GetJobLogs(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
+ _, handler := ActionsGetJobLogs(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
request := createMCPRequest(map[string]any{
"owner": "owner",
"repo": "repo",
"job_id": float64(123),
"return_content": true,
- "tail_lines": float64(1), // Requesting last 1 line
+ "tail_lines": float64(1),
})
args := map[string]any{
"owner": "owner",
@@ -1196,10 +1075,10 @@ func Test_GetJobLogs_WithContentReturnAndTailLines(t *testing.T) {
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
+ assert.NotContains(t, response, "logs_url")
}
-func Test_GetJobLogs_WithContentReturnAndLargeTailLines(t *testing.T) {
+func Test_ActionsGetJobLogs_WithContentReturnAndLargeTailLines(t *testing.T) {
logContent := "Line 1\nLine 2\nLine 3"
expectedLogContent := "Line 1\nLine 2\nLine 3"
@@ -1220,7 +1099,7 @@ func Test_GetJobLogs_WithContentReturnAndLargeTailLines(t *testing.T) {
)
client := github.NewClient(mockedClient)
- _, handler := GetJobLogs(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
+ _, handler := ActionsGetJobLogs(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
request := createMCPRequest(map[string]any{
"owner": "owner",
@@ -1351,92 +1230,87 @@ func Test_MemoryUsage_SlidingWindow_vs_NoWindow(t *testing.T) {
t.Logf("No window: %s", profile2.String())
}
-func Test_ListWorkflowRuns(t *testing.T) {
- // Verify tool definition once
- mockClient := github.NewClient(nil)
- tool, _ := ListWorkflowRuns(stubGetClientFn(mockClient), translations.NullTranslationHelper)
- require.NoError(t, toolsnaps.Test(tool.Name, tool))
-
- assert.Equal(t, "list_workflow_runs", tool.Name)
- assert.NotEmpty(t, tool.Description)
- inputSchema := 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
- mockClient := github.NewClient(nil)
- tool, _ := GetWorkflowRun(stubGetClientFn(mockClient), translations.NullTranslationHelper)
- require.NoError(t, toolsnaps.Test(tool.Name, tool))
-
- assert.Equal(t, "get_workflow_run", tool.Name)
- assert.NotEmpty(t, tool.Description)
- inputSchema := 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
- mockClient := github.NewClient(nil)
- tool, _ := GetWorkflowRunLogs(stubGetClientFn(mockClient), translations.NullTranslationHelper)
- require.NoError(t, toolsnaps.Test(tool.Name, tool))
-
- assert.Equal(t, "get_workflow_run_logs", tool.Name)
- assert.NotEmpty(t, tool.Description)
- inputSchema := 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
- mockClient := github.NewClient(nil)
- tool, _ := ListWorkflowJobs(stubGetClientFn(mockClient), translations.NullTranslationHelper)
- require.NoError(t, toolsnaps.Test(tool.Name, tool))
+func Test_ActionsGet_GetWorkflowRunUsage(t *testing.T) {
+ tests := []struct {
+ name string
+ mockedClient *http.Client
+ requestArgs map[string]any
+ expectError bool
+ expectedErrMsg string
+ }{
+ {
+ name: "successful workflow run usage",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.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{
+ "method": "get_workflow_run_usage",
+ "owner": "owner",
+ "repo": "repo",
+ "resource_id": "12345",
+ },
+ expectError: false,
+ },
+ {
+ name: "missing required parameter resource_id",
+ mockedClient: mock.NewMockedHTTPClient(),
+ requestArgs: map[string]any{
+ "method": "get_workflow_run_usage",
+ "owner": "owner",
+ "repo": "repo",
+ },
+ expectError: true,
+ expectedErrMsg: "missing required parameter: resource_id",
+ },
+ }
- assert.Equal(t, "list_workflow_jobs", tool.Name)
- assert.NotEmpty(t, tool.Description)
- inputSchema := 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"})
-}
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ client := github.NewClient(tc.mockedClient)
+ _, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper)
+ request := createMCPRequest(tc.requestArgs)
+ result, _, err := handler(context.Background(), &request, tc.requestArgs)
-func Test_RerunWorkflowRun(t *testing.T) {
- // Verify tool definition once
- mockClient := github.NewClient(nil)
- tool, _ := RerunWorkflowRun(stubGetClientFn(mockClient), translations.NullTranslationHelper)
- require.NoError(t, toolsnaps.Test(tool.Name, tool))
+ require.NoError(t, err)
+ require.Equal(t, tc.expectError, result.IsError)
- assert.Equal(t, "rerun_workflow_run", tool.Name)
- assert.NotEmpty(t, tool.Description)
- inputSchema := 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"})
-}
+ textContent := getTextResult(t, result)
-func Test_RerunFailedJobs(t *testing.T) {
- // Verify tool definition once
- mockClient := github.NewClient(nil)
- tool, _ := RerunFailedJobs(stubGetClientFn(mockClient), translations.NullTranslationHelper)
- require.NoError(t, toolsnaps.Test(tool.Name, tool))
+ if tc.expectedErrMsg != "" {
+ assert.Equal(t, tc.expectedErrMsg, textContent.Text)
+ return
+ }
- assert.Equal(t, "rerun_failed_jobs", tool.Name)
- assert.NotEmpty(t, tool.Description)
- inputSchema := 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"})
+ 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)
+ })
+ }
}
diff --git a/pkg/github/deprecated_tool_aliases.go b/pkg/github/deprecated_tool_aliases.go
index 4abdca14d..d83db1ed6 100644
--- a/pkg/github/deprecated_tool_aliases.go
+++ b/pkg/github/deprecated_tool_aliases.go
@@ -10,5 +10,20 @@ package github
// "get_issue": "issue_read",
// "create_pr": "pull_request_create",
var DeprecatedToolAliases = map[string]string{
- // Add entries as tools are renamed
+ // Actions tools consolidated
+ "list_workflows": "actions_list",
+ "list_workflow_runs": "actions_list",
+ "list_workflow_jobs": "actions_list",
+ "list_workflow_run_artifacts": "actions_list",
+ "get_workflow": "actions_get",
+ "get_workflow_run": "actions_get",
+ "get_workflow_job": "actions_get",
+ "get_workflow_run_usage": "actions_get",
+ "get_workflow_run_logs": "actions_get",
+ "download_workflow_run_artifact": "actions_get",
+ "run_workflow": "actions_run_trigger",
+ "rerun_workflow_run": "actions_run_trigger",
+ "rerun_failed_jobs": "actions_run_trigger",
+ "cancel_workflow_run": "actions_run_trigger",
+ "delete_workflow_run_logs": "actions_run_trigger",
}
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index f21a9ae5b..1f874f789 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -277,22 +277,12 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
actions := toolsets.NewToolset(ToolsetMetadataActions.ID, ToolsetMetadataActions.Description).
AddReadTools(
- toolsets.NewServerTool(ListWorkflows(getClient, t)),
- toolsets.NewServerTool(ListWorkflowRuns(getClient, t)),
- toolsets.NewServerTool(GetWorkflowRun(getClient, t)),
- toolsets.NewServerTool(GetWorkflowRunLogs(getClient, t)),
- toolsets.NewServerTool(ListWorkflowJobs(getClient, t)),
- toolsets.NewServerTool(GetJobLogs(getClient, t, contentWindowSize)),
- toolsets.NewServerTool(ListWorkflowRunArtifacts(getClient, t)),
- toolsets.NewServerTool(DownloadWorkflowRunArtifact(getClient, t)),
- toolsets.NewServerTool(GetWorkflowRunUsage(getClient, t)),
+ toolsets.NewServerTool(ActionsGet(getClient, t)),
+ toolsets.NewServerTool(ActionsList(getClient, t)),
+ toolsets.NewServerTool(ActionsGetJobLogs(getClient, t, contentWindowSize)),
).
AddWriteTools(
- toolsets.NewServerTool(RunWorkflow(getClient, t)),
- toolsets.NewServerTool(RerunWorkflowRun(getClient, t)),
- toolsets.NewServerTool(RerunFailedJobs(getClient, t)),
- toolsets.NewServerTool(CancelWorkflowRun(getClient, t)),
- toolsets.NewServerTool(DeleteWorkflowRunLogs(getClient, t)),
+ toolsets.NewServerTool(ActionsRunTrigger(getClient, t)),
)
securityAdvisories := toolsets.NewToolset(ToolsetMetadataSecurityAdvisories.ID, ToolsetMetadataSecurityAdvisories.Description).