Spaces:
Build error
Build error
| package github | |
| import ( | |
| "context" | |
| "encoding/json" | |
| "fmt" | |
| "net/http" | |
| "strconv" | |
| "strings" | |
| "github.com/github/github-mcp-server/internal/profiler" | |
| buffer "github.com/github/github-mcp-server/pkg/buffer" | |
| ghErrors "github.com/github/github-mcp-server/pkg/errors" | |
| "github.com/github/github-mcp-server/pkg/translations" | |
| "github.com/google/go-github/v74/github" | |
| "github.com/mark3labs/mcp-go/mcp" | |
| "github.com/mark3labs/mcp-go/server" | |
| ) | |
| const ( | |
| DescriptionRepositoryOwner = "Repository owner" | |
| DescriptionRepositoryName = "Repository name" | |
| ) | |
| // ListWorkflows creates a tool to list workflows in a repository | |
| func ListWorkflows(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | |
| return mcp.NewTool("list_workflows", | |
| mcp.WithDescription(t("TOOL_LIST_WORKFLOWS_DESCRIPTION", "List workflows in a repository")), | |
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ | |
| Title: t("TOOL_LIST_WORKFLOWS_USER_TITLE", "List workflows"), | |
| ReadOnlyHint: ToBoolPtr(true), | |
| }), | |
| mcp.WithString("owner", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryOwner), | |
| ), | |
| mcp.WithString("repo", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryName), | |
| ), | |
| WithPagination(), | |
| ), | |
| func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
| owner, err := RequiredParam[string](request, "owner") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| repo, err := RequiredParam[string](request, "repo") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| // Get optional pagination parameters | |
| pagination, err := OptionalPaginationParams(request) | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| client, err := getClient(ctx) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to get GitHub client: %w", err) | |
| } | |
| // 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, fmt.Errorf("failed to list workflows: %w", err) | |
| } | |
| defer func() { _ = resp.Body.Close() }() | |
| r, err := json.Marshal(workflows) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| } | |
| // ListWorkflowRuns creates a tool to list workflow runs for a specific workflow | |
| func ListWorkflowRuns(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | |
| return mcp.NewTool("list_workflow_runs", | |
| mcp.WithDescription(t("TOOL_LIST_WORKFLOW_RUNS_DESCRIPTION", "List workflow runs for a specific workflow")), | |
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ | |
| Title: t("TOOL_LIST_WORKFLOW_RUNS_USER_TITLE", "List workflow runs"), | |
| ReadOnlyHint: ToBoolPtr(true), | |
| }), | |
| mcp.WithString("owner", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryOwner), | |
| ), | |
| mcp.WithString("repo", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryName), | |
| ), | |
| mcp.WithString("workflow_id", | |
| mcp.Required(), | |
| mcp.Description("The workflow ID or workflow file name"), | |
| ), | |
| mcp.WithString("actor", | |
| mcp.Description("Returns someone's workflow runs. Use the login for the user who created the workflow run."), | |
| ), | |
| mcp.WithString("branch", | |
| mcp.Description("Returns workflow runs associated with a branch. Use the name of the branch."), | |
| ), | |
| mcp.WithString("event", | |
| mcp.Description("Returns workflow runs for a specific event type"), | |
| mcp.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", | |
| ), | |
| ), | |
| mcp.WithString("status", | |
| mcp.Description("Returns workflow runs with the check run status"), | |
| mcp.Enum("queued", "in_progress", "completed", "requested", "waiting"), | |
| ), | |
| WithPagination(), | |
| ), | |
| func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
| owner, err := RequiredParam[string](request, "owner") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| repo, err := RequiredParam[string](request, "repo") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| workflowID, err := RequiredParam[string](request, "workflow_id") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| // Get optional filtering parameters | |
| actor, err := OptionalParam[string](request, "actor") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| branch, err := OptionalParam[string](request, "branch") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| event, err := OptionalParam[string](request, "event") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| status, err := OptionalParam[string](request, "status") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| // Get optional pagination parameters | |
| pagination, err := OptionalPaginationParams(request) | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| client, err := getClient(ctx) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to get GitHub client: %w", err) | |
| } | |
| // Set up list options | |
| opts := &github.ListWorkflowRunsOptions{ | |
| Actor: actor, | |
| Branch: branch, | |
| Event: event, | |
| Status: status, | |
| ListOptions: github.ListOptions{ | |
| PerPage: pagination.PerPage, | |
| Page: pagination.Page, | |
| }, | |
| } | |
| workflowRuns, resp, err := client.Actions.ListWorkflowRunsByFileName(ctx, owner, repo, workflowID, opts) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to list workflow runs: %w", err) | |
| } | |
| defer func() { _ = resp.Body.Close() }() | |
| r, err := json.Marshal(workflowRuns) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| } | |
| // RunWorkflow creates a tool to run an Actions workflow | |
| func RunWorkflow(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | |
| return mcp.NewTool("run_workflow", | |
| mcp.WithDescription(t("TOOL_RUN_WORKFLOW_DESCRIPTION", "Run an Actions workflow by workflow ID or filename")), | |
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ | |
| Title: t("TOOL_RUN_WORKFLOW_USER_TITLE", "Run workflow"), | |
| ReadOnlyHint: ToBoolPtr(false), | |
| }), | |
| mcp.WithString("owner", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryOwner), | |
| ), | |
| mcp.WithString("repo", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryName), | |
| ), | |
| mcp.WithString("workflow_id", | |
| mcp.Required(), | |
| mcp.Description("The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml)"), | |
| ), | |
| mcp.WithString("ref", | |
| mcp.Required(), | |
| mcp.Description("The git reference for the workflow. The reference can be a branch or tag name."), | |
| ), | |
| mcp.WithObject("inputs", | |
| mcp.Description("Inputs the workflow accepts"), | |
| ), | |
| ), | |
| func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
| owner, err := RequiredParam[string](request, "owner") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| repo, err := RequiredParam[string](request, "repo") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| workflowID, err := RequiredParam[string](request, "workflow_id") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| ref, err := RequiredParam[string](request, "ref") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| // Get optional inputs parameter | |
| var inputs map[string]interface{} | |
| if requestInputs, ok := request.GetArguments()["inputs"]; ok { | |
| if inputsMap, ok := requestInputs.(map[string]interface{}); ok { | |
| inputs = inputsMap | |
| } | |
| } | |
| client, err := getClient(ctx) | |
| if err != nil { | |
| return 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, fmt.Errorf("failed to run workflow: %w", err) | |
| } | |
| defer func() { _ = resp.Body.Close() }() | |
| result := map[string]any{ | |
| "message": "Workflow run has been queued", | |
| "workflow_type": workflowType, | |
| "workflow_id": workflowID, | |
| "ref": ref, | |
| "inputs": inputs, | |
| "status": resp.Status, | |
| "status_code": resp.StatusCode, | |
| } | |
| r, err := json.Marshal(result) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| } | |
| // GetWorkflowRun creates a tool to get details of a specific workflow run | |
| func GetWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | |
| return mcp.NewTool("get_workflow_run", | |
| mcp.WithDescription(t("TOOL_GET_WORKFLOW_RUN_DESCRIPTION", "Get details of a specific workflow run")), | |
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ | |
| Title: t("TOOL_GET_WORKFLOW_RUN_USER_TITLE", "Get workflow run"), | |
| ReadOnlyHint: ToBoolPtr(true), | |
| }), | |
| mcp.WithString("owner", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryOwner), | |
| ), | |
| mcp.WithString("repo", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryName), | |
| ), | |
| mcp.WithNumber("run_id", | |
| mcp.Required(), | |
| mcp.Description("The unique identifier of the workflow run"), | |
| ), | |
| ), | |
| func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
| owner, err := RequiredParam[string](request, "owner") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| repo, err := RequiredParam[string](request, "repo") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runIDInt, err := RequiredInt(request, "run_id") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runID := int64(runIDInt) | |
| client, err := getClient(ctx) | |
| if err != nil { | |
| return 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, fmt.Errorf("failed to get workflow run: %w", err) | |
| } | |
| defer func() { _ = resp.Body.Close() }() | |
| r, err := json.Marshal(workflowRun) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| } | |
| // GetWorkflowRunLogs creates a tool to download logs for a specific workflow run | |
| func GetWorkflowRunLogs(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | |
| return mcp.NewTool("get_workflow_run_logs", | |
| mcp.WithDescription(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)")), | |
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ | |
| Title: t("TOOL_GET_WORKFLOW_RUN_LOGS_USER_TITLE", "Get workflow run logs"), | |
| ReadOnlyHint: ToBoolPtr(true), | |
| }), | |
| mcp.WithString("owner", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryOwner), | |
| ), | |
| mcp.WithString("repo", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryName), | |
| ), | |
| mcp.WithNumber("run_id", | |
| mcp.Required(), | |
| mcp.Description("The unique identifier of the workflow run"), | |
| ), | |
| ), | |
| func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
| owner, err := RequiredParam[string](request, "owner") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| repo, err := RequiredParam[string](request, "repo") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runIDInt, err := RequiredInt(request, "run_id") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runID := int64(runIDInt) | |
| client, err := getClient(ctx) | |
| if err != nil { | |
| return 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, 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, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| } | |
| // ListWorkflowJobs creates a tool to list jobs for a specific workflow run | |
| func ListWorkflowJobs(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | |
| return mcp.NewTool("list_workflow_jobs", | |
| mcp.WithDescription(t("TOOL_LIST_WORKFLOW_JOBS_DESCRIPTION", "List jobs for a specific workflow run")), | |
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ | |
| Title: t("TOOL_LIST_WORKFLOW_JOBS_USER_TITLE", "List workflow jobs"), | |
| ReadOnlyHint: ToBoolPtr(true), | |
| }), | |
| mcp.WithString("owner", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryOwner), | |
| ), | |
| mcp.WithString("repo", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryName), | |
| ), | |
| mcp.WithNumber("run_id", | |
| mcp.Required(), | |
| mcp.Description("The unique identifier of the workflow run"), | |
| ), | |
| mcp.WithString("filter", | |
| mcp.Description("Filters jobs by their completed_at timestamp"), | |
| mcp.Enum("latest", "all"), | |
| ), | |
| WithPagination(), | |
| ), | |
| func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
| owner, err := RequiredParam[string](request, "owner") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| repo, err := RequiredParam[string](request, "repo") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runIDInt, err := RequiredInt(request, "run_id") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runID := int64(runIDInt) | |
| // Get optional filtering parameters | |
| filter, err := OptionalParam[string](request, "filter") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| // Get optional pagination parameters | |
| pagination, err := OptionalPaginationParams(request) | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| client, err := getClient(ctx) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to get GitHub client: %w", err) | |
| } | |
| // 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, fmt.Errorf("failed to list workflow jobs: %w", err) | |
| } | |
| defer func() { _ = resp.Body.Close() }() | |
| // Add optimization tip for failed job debugging | |
| response := map[string]any{ | |
| "jobs": jobs, | |
| "optimization_tip": "For debugging failed jobs, consider using get_job_logs with failed_only=true and run_id=" + fmt.Sprintf("%d", runID) + " to get logs directly without needing to list jobs first", | |
| } | |
| r, err := json.Marshal(response) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), 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) (tool mcp.Tool, handler server.ToolHandlerFunc) { | |
| return mcp.NewTool("get_job_logs", | |
| mcp.WithDescription(t("TOOL_GET_JOB_LOGS_DESCRIPTION", "Download logs for a specific workflow job or efficiently get all failed job logs for a workflow run")), | |
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ | |
| Title: t("TOOL_GET_JOB_LOGS_USER_TITLE", "Get job logs"), | |
| ReadOnlyHint: ToBoolPtr(true), | |
| }), | |
| mcp.WithString("owner", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryOwner), | |
| ), | |
| mcp.WithString("repo", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryName), | |
| ), | |
| mcp.WithNumber("job_id", | |
| mcp.Description("The unique identifier of the workflow job (required for single job logs)"), | |
| ), | |
| mcp.WithNumber("run_id", | |
| mcp.Description("Workflow run ID (required when using failed_only)"), | |
| ), | |
| mcp.WithBoolean("failed_only", | |
| mcp.Description("When true, gets logs for all failed jobs in run_id"), | |
| ), | |
| mcp.WithBoolean("return_content", | |
| mcp.Description("Returns actual log content instead of URLs"), | |
| ), | |
| mcp.WithNumber("tail_lines", | |
| mcp.Description("Number of lines to return from the end of the log"), | |
| mcp.DefaultNumber(500), | |
| ), | |
| ), | |
| func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
| owner, err := RequiredParam[string](request, "owner") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| repo, err := RequiredParam[string](request, "repo") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| // Get optional parameters | |
| jobID, err := OptionalIntParam(request, "job_id") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runID, err := OptionalIntParam(request, "run_id") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| failedOnly, err := OptionalParam[bool](request, "failed_only") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| returnContent, err := OptionalParam[bool](request, "return_content") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| tailLines, err := OptionalIntParam(request, "tail_lines") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| // Default to 500 lines if not specified | |
| if tailLines == 0 { | |
| tailLines = 500 | |
| } | |
| client, err := getClient(ctx) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to get GitHub client: %w", err) | |
| } | |
| // Validate parameters | |
| if failedOnly && runID == 0 { | |
| return mcp.NewToolResultError("run_id is required when failed_only is true"), nil | |
| } | |
| if !failedOnly && jobID == 0 { | |
| return mcp.NewToolResultError("job_id is required when failed_only is false"), nil | |
| } | |
| if failedOnly && runID > 0 { | |
| // Handle failed-only mode: get logs for all failed jobs in the workflow run | |
| return handleFailedJobLogs(ctx, client, owner, repo, int64(runID), returnContent, tailLines, contentWindowSize) | |
| } else if jobID > 0 { | |
| // Handle single job mode | |
| return handleSingleJobLogs(ctx, client, owner, repo, int64(jobID), returnContent, tailLines, contentWindowSize) | |
| } | |
| return mcp.NewToolResultError("Either job_id must be provided for single job logs, or run_id with failed_only=true for failed job logs"), nil | |
| } | |
| } | |
| // handleFailedJobLogs gets logs for all failed jobs in a workflow run | |
| func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo string, runID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, 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 | |
| } | |
| defer func() { _ = resp.Body.Close() }() | |
| // Filter for failed jobs | |
| var failedJobs []*github.WorkflowJob | |
| for _, job := range jobs.Jobs { | |
| if job.GetConclusion() == "failure" { | |
| failedJobs = append(failedJobs, job) | |
| } | |
| } | |
| if len(failedJobs) == 0 { | |
| result := map[string]any{ | |
| "message": "No failed jobs found in this workflow run", | |
| "run_id": runID, | |
| "total_jobs": len(jobs.Jobs), | |
| "failed_jobs": 0, | |
| } | |
| r, _ := json.Marshal(result) | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| // Collect logs for all failed jobs | |
| var logResults []map[string]any | |
| for _, job := range failedJobs { | |
| jobResult, resp, err := getJobLogData(ctx, client, owner, repo, job.GetID(), job.GetName(), returnContent, tailLines, contentWindowSize) | |
| if err != nil { | |
| // Continue with other jobs even if one fails | |
| jobResult = map[string]any{ | |
| "job_id": job.GetID(), | |
| "job_name": job.GetName(), | |
| "error": err.Error(), | |
| } | |
| // Enable reporting of status codes and error causes | |
| _, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get job logs", resp, err) // Explicitly ignore error for graceful handling | |
| } | |
| logResults = append(logResults, jobResult) | |
| } | |
| result := map[string]any{ | |
| "message": fmt.Sprintf("Retrieved logs for %d failed jobs", len(failedJobs)), | |
| "run_id": runID, | |
| "total_jobs": len(jobs.Jobs), | |
| "failed_jobs": len(failedJobs), | |
| "logs": logResults, | |
| "return_format": map[string]bool{"content": returnContent, "urls": !returnContent}, | |
| } | |
| r, err := json.Marshal(result) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| // handleSingleJobLogs gets logs for a single job | |
| func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo string, jobID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, 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 | |
| } | |
| r, err := json.Marshal(jobResult) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| // getJobLogData retrieves log data for a single job, either as URL or content | |
| func getJobLogData(ctx context.Context, client *github.Client, owner, repo string, jobID int64, jobName string, returnContent bool, tailLines int, contentWindowSize int) (map[string]any, *github.Response, error) { | |
| // Get the download URL for the job logs | |
| url, resp, err := client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 1) | |
| if err != nil { | |
| return nil, resp, fmt.Errorf("failed to get job logs for job %d: %w", jobID, err) | |
| } | |
| defer func() { _ = resp.Body.Close() }() | |
| result := map[string]any{ | |
| "job_id": jobID, | |
| } | |
| if jobName != "" { | |
| result["job_name"] = jobName | |
| } | |
| if returnContent { | |
| // Download and return the actual log content | |
| content, originalLength, httpResp, err := downloadLogContent(ctx, url.String(), tailLines, contentWindowSize) //nolint:bodyclose // Response body is closed in downloadLogContent, but we need to return httpResp | |
| if err != nil { | |
| // To keep the return value consistent wrap the response as a GitHub Response | |
| ghRes := &github.Response{ | |
| Response: httpResp, | |
| } | |
| return nil, ghRes, fmt.Errorf("failed to download log content for job %d: %w", jobID, err) | |
| } | |
| result["logs_content"] = content | |
| result["message"] = "Job logs content retrieved successfully" | |
| result["original_length"] = originalLength | |
| } else { | |
| // Return just the URL | |
| result["logs_url"] = url.String() | |
| result["message"] = "Job logs are available for download" | |
| result["note"] = "The logs_url provides a download link for the individual job logs in plain text format. Use return_content=true to get the actual log content." | |
| } | |
| return result, resp, nil | |
| } | |
| func downloadLogContent(ctx context.Context, logURL string, tailLines int, maxLines int) (string, int, *http.Response, error) { | |
| prof := profiler.New(nil, profiler.IsProfilingEnabled()) | |
| finish := prof.Start(ctx, "log_buffer_processing") | |
| httpResp, err := http.Get(logURL) //nolint:gosec | |
| if err != nil { | |
| return "", 0, httpResp, fmt.Errorf("failed to download logs: %w", err) | |
| } | |
| defer func() { _ = httpResp.Body.Close() }() | |
| if httpResp.StatusCode != http.StatusOK { | |
| return "", 0, httpResp, fmt.Errorf("failed to download logs: HTTP %d", httpResp.StatusCode) | |
| } | |
| bufferSize := tailLines | |
| if bufferSize > maxLines { | |
| bufferSize = maxLines | |
| } | |
| processedInput, totalLines, httpResp, err := buffer.ProcessResponseAsRingBufferToEnd(httpResp, bufferSize) | |
| if err != nil { | |
| return "", 0, httpResp, fmt.Errorf("failed to process log content: %w", err) | |
| } | |
| lines := strings.Split(processedInput, "\n") | |
| if len(lines) > tailLines { | |
| lines = lines[len(lines)-tailLines:] | |
| } | |
| finalResult := strings.Join(lines, "\n") | |
| _ = finish(len(lines), int64(len(finalResult))) | |
| return finalResult, totalLines, httpResp, nil | |
| } | |
| // RerunWorkflowRun creates a tool to re-run an entire workflow run | |
| func RerunWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | |
| return mcp.NewTool("rerun_workflow_run", | |
| mcp.WithDescription(t("TOOL_RERUN_WORKFLOW_RUN_DESCRIPTION", "Re-run an entire workflow run")), | |
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ | |
| Title: t("TOOL_RERUN_WORKFLOW_RUN_USER_TITLE", "Rerun workflow run"), | |
| ReadOnlyHint: ToBoolPtr(false), | |
| }), | |
| mcp.WithString("owner", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryOwner), | |
| ), | |
| mcp.WithString("repo", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryName), | |
| ), | |
| mcp.WithNumber("run_id", | |
| mcp.Required(), | |
| mcp.Description("The unique identifier of the workflow run"), | |
| ), | |
| ), | |
| func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
| owner, err := RequiredParam[string](request, "owner") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| repo, err := RequiredParam[string](request, "repo") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runIDInt, err := RequiredInt(request, "run_id") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runID := int64(runIDInt) | |
| client, err := getClient(ctx) | |
| if err != nil { | |
| return 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 | |
| } | |
| 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, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| } | |
| // RerunFailedJobs creates a tool to re-run only the failed jobs in a workflow run | |
| func RerunFailedJobs(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | |
| return mcp.NewTool("rerun_failed_jobs", | |
| mcp.WithDescription(t("TOOL_RERUN_FAILED_JOBS_DESCRIPTION", "Re-run only the failed jobs in a workflow run")), | |
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ | |
| Title: t("TOOL_RERUN_FAILED_JOBS_USER_TITLE", "Rerun failed jobs"), | |
| ReadOnlyHint: ToBoolPtr(false), | |
| }), | |
| mcp.WithString("owner", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryOwner), | |
| ), | |
| mcp.WithString("repo", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryName), | |
| ), | |
| mcp.WithNumber("run_id", | |
| mcp.Required(), | |
| mcp.Description("The unique identifier of the workflow run"), | |
| ), | |
| ), | |
| func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
| owner, err := RequiredParam[string](request, "owner") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| repo, err := RequiredParam[string](request, "repo") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runIDInt, err := RequiredInt(request, "run_id") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runID := int64(runIDInt) | |
| client, err := getClient(ctx) | |
| if err != nil { | |
| return 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 | |
| } | |
| 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, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| } | |
| // CancelWorkflowRun creates a tool to cancel a workflow run | |
| func CancelWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | |
| return mcp.NewTool("cancel_workflow_run", | |
| mcp.WithDescription(t("TOOL_CANCEL_WORKFLOW_RUN_DESCRIPTION", "Cancel a workflow run")), | |
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ | |
| Title: t("TOOL_CANCEL_WORKFLOW_RUN_USER_TITLE", "Cancel workflow run"), | |
| ReadOnlyHint: ToBoolPtr(false), | |
| }), | |
| mcp.WithString("owner", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryOwner), | |
| ), | |
| mcp.WithString("repo", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryName), | |
| ), | |
| mcp.WithNumber("run_id", | |
| mcp.Required(), | |
| mcp.Description("The unique identifier of the workflow run"), | |
| ), | |
| ), | |
| func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
| owner, err := RequiredParam[string](request, "owner") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| repo, err := RequiredParam[string](request, "repo") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runIDInt, err := RequiredInt(request, "run_id") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runID := int64(runIDInt) | |
| client, err := getClient(ctx) | |
| if err != nil { | |
| return 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 | |
| } | |
| } | |
| 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, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| } | |
| // ListWorkflowRunArtifacts creates a tool to list artifacts for a workflow run | |
| func ListWorkflowRunArtifacts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | |
| return mcp.NewTool("list_workflow_run_artifacts", | |
| mcp.WithDescription(t("TOOL_LIST_WORKFLOW_RUN_ARTIFACTS_DESCRIPTION", "List artifacts for a workflow run")), | |
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ | |
| Title: t("TOOL_LIST_WORKFLOW_RUN_ARTIFACTS_USER_TITLE", "List workflow artifacts"), | |
| ReadOnlyHint: ToBoolPtr(true), | |
| }), | |
| mcp.WithString("owner", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryOwner), | |
| ), | |
| mcp.WithString("repo", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryName), | |
| ), | |
| mcp.WithNumber("run_id", | |
| mcp.Required(), | |
| mcp.Description("The unique identifier of the workflow run"), | |
| ), | |
| WithPagination(), | |
| ), | |
| func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
| owner, err := RequiredParam[string](request, "owner") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| repo, err := RequiredParam[string](request, "repo") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runIDInt, err := RequiredInt(request, "run_id") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runID := int64(runIDInt) | |
| // Get optional pagination parameters | |
| pagination, err := OptionalPaginationParams(request) | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| client, err := getClient(ctx) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to get GitHub client: %w", err) | |
| } | |
| // 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 | |
| } | |
| defer func() { _ = resp.Body.Close() }() | |
| r, err := json.Marshal(artifacts) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| } | |
| // DownloadWorkflowRunArtifact creates a tool to download a workflow run artifact | |
| func DownloadWorkflowRunArtifact(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | |
| return mcp.NewTool("download_workflow_run_artifact", | |
| mcp.WithDescription(t("TOOL_DOWNLOAD_WORKFLOW_RUN_ARTIFACT_DESCRIPTION", "Get download URL for a workflow run artifact")), | |
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ | |
| Title: t("TOOL_DOWNLOAD_WORKFLOW_RUN_ARTIFACT_USER_TITLE", "Download workflow artifact"), | |
| ReadOnlyHint: ToBoolPtr(true), | |
| }), | |
| mcp.WithString("owner", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryOwner), | |
| ), | |
| mcp.WithString("repo", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryName), | |
| ), | |
| mcp.WithNumber("artifact_id", | |
| mcp.Required(), | |
| mcp.Description("The unique identifier of the artifact"), | |
| ), | |
| ), | |
| func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
| owner, err := RequiredParam[string](request, "owner") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| repo, err := RequiredParam[string](request, "repo") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| artifactIDInt, err := RequiredInt(request, "artifact_id") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| artifactID := int64(artifactIDInt) | |
| client, err := getClient(ctx) | |
| if err != nil { | |
| return 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 | |
| } | |
| 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, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| } | |
| // DeleteWorkflowRunLogs creates a tool to delete logs for a workflow run | |
| func DeleteWorkflowRunLogs(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | |
| return mcp.NewTool("delete_workflow_run_logs", | |
| mcp.WithDescription(t("TOOL_DELETE_WORKFLOW_RUN_LOGS_DESCRIPTION", "Delete logs for a workflow run")), | |
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ | |
| Title: t("TOOL_DELETE_WORKFLOW_RUN_LOGS_USER_TITLE", "Delete workflow logs"), | |
| ReadOnlyHint: ToBoolPtr(false), | |
| DestructiveHint: ToBoolPtr(true), | |
| }), | |
| mcp.WithString("owner", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryOwner), | |
| ), | |
| mcp.WithString("repo", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryName), | |
| ), | |
| mcp.WithNumber("run_id", | |
| mcp.Required(), | |
| mcp.Description("The unique identifier of the workflow run"), | |
| ), | |
| ), | |
| func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
| owner, err := RequiredParam[string](request, "owner") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| repo, err := RequiredParam[string](request, "repo") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runIDInt, err := RequiredInt(request, "run_id") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runID := int64(runIDInt) | |
| client, err := getClient(ctx) | |
| if err != nil { | |
| return 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 | |
| } | |
| 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, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| } | |
| // GetWorkflowRunUsage creates a tool to get usage metrics for a workflow run | |
| func GetWorkflowRunUsage(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | |
| return mcp.NewTool("get_workflow_run_usage", | |
| mcp.WithDescription(t("TOOL_GET_WORKFLOW_RUN_USAGE_DESCRIPTION", "Get usage metrics for a workflow run")), | |
| mcp.WithToolAnnotation(mcp.ToolAnnotation{ | |
| Title: t("TOOL_GET_WORKFLOW_RUN_USAGE_USER_TITLE", "Get workflow usage"), | |
| ReadOnlyHint: ToBoolPtr(true), | |
| }), | |
| mcp.WithString("owner", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryOwner), | |
| ), | |
| mcp.WithString("repo", | |
| mcp.Required(), | |
| mcp.Description(DescriptionRepositoryName), | |
| ), | |
| mcp.WithNumber("run_id", | |
| mcp.Required(), | |
| mcp.Description("The unique identifier of the workflow run"), | |
| ), | |
| ), | |
| func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
| owner, err := RequiredParam[string](request, "owner") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| repo, err := RequiredParam[string](request, "repo") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runIDInt, err := RequiredInt(request, "run_id") | |
| if err != nil { | |
| return mcp.NewToolResultError(err.Error()), nil | |
| } | |
| runID := int64(runIDInt) | |
| client, err := getClient(ctx) | |
| if err != nil { | |
| return 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 | |
| } | |
| defer func() { _ = resp.Body.Close() }() | |
| r, err := json.Marshal(usage) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to marshal response: %w", err) | |
| } | |
| return mcp.NewToolResultText(string(r)), nil | |
| } | |
| } | |