bab6b7c971
Build and Test Go Project / build (ubuntu-latest) (push) Has been cancelled
Build and Test Go Project / build (windows-latest) (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Docker / build (push) Has been cancelled
Build and Test Go Project / build (macos-latest) (push) Has been cancelled
GoReleaser Release / release (push) Has been cancelled
License Check / license-check (push) Has been cancelled
Publish to MCP Registry / publish (push) Has been cancelled
Update remaining references to 'registry' in code comments to use 'inventory' consistently after the package rename.
1402 lines
46 KiB
Go
1402 lines
46 KiB
Go
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/inventory"
|
|
"github.com/github/github-mcp-server/pkg/translations"
|
|
"github.com/github/github-mcp-server/pkg/utils"
|
|
"github.com/google/go-github/v79/github"
|
|
"github.com/google/jsonschema-go/jsonschema"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
const (
|
|
DescriptionRepositoryOwner = "Repository owner"
|
|
DescriptionRepositoryName = "Repository name"
|
|
)
|
|
|
|
// ListWorkflows creates a tool to list workflows in a repository
|
|
func ListWorkflows(t translations.TranslationHelperFunc) inventory.ServerTool {
|
|
return NewTool(
|
|
ToolsetMetadataActions,
|
|
mcp.Tool{
|
|
Name: "list_workflows",
|
|
Description: t("TOOL_LIST_WORKFLOWS_DESCRIPTION", "List workflows in a repository"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_LIST_WORKFLOWS_USER_TITLE", "List workflows"),
|
|
ReadOnlyHint: true,
|
|
},
|
|
InputSchema: WithPagination(&jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryOwner,
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryName,
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo"},
|
|
}),
|
|
},
|
|
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
|
|
}
|
|
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
|
|
// Get optional pagination parameters
|
|
pagination, err := OptionalPaginationParams(args)
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
|
|
// Set up list options
|
|
opts := &github.ListOptions{
|
|
PerPage: pagination.PerPage,
|
|
Page: pagination.Page,
|
|
}
|
|
|
|
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(t translations.TranslationHelperFunc) inventory.ServerTool {
|
|
return NewTool(
|
|
ToolsetMetadataActions,
|
|
mcp.Tool{
|
|
Name: "list_workflow_runs",
|
|
Description: t("TOOL_LIST_WORKFLOW_RUNS_DESCRIPTION", "List workflow runs for a specific workflow"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_LIST_WORKFLOW_RUNS_USER_TITLE", "List workflow runs"),
|
|
ReadOnlyHint: true,
|
|
},
|
|
InputSchema: WithPagination(&jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryOwner,
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryName,
|
|
},
|
|
"workflow_id": {
|
|
Type: "string",
|
|
Description: "The workflow ID or workflow file name",
|
|
},
|
|
"actor": {
|
|
Type: "string",
|
|
Description: "Returns someone's workflow runs. Use the login for the user who created the workflow run.",
|
|
},
|
|
"branch": {
|
|
Type: "string",
|
|
Description: "Returns workflow runs associated with a branch. Use the name of the branch.",
|
|
},
|
|
"event": {
|
|
Type: "string",
|
|
Description: "Returns workflow runs for a specific event type",
|
|
Enum: []any{
|
|
"branch_protection_rule",
|
|
"check_run",
|
|
"check_suite",
|
|
"create",
|
|
"delete",
|
|
"deployment",
|
|
"deployment_status",
|
|
"discussion",
|
|
"discussion_comment",
|
|
"fork",
|
|
"gollum",
|
|
"issue_comment",
|
|
"issues",
|
|
"label",
|
|
"merge_group",
|
|
"milestone",
|
|
"page_build",
|
|
"public",
|
|
"pull_request",
|
|
"pull_request_review",
|
|
"pull_request_review_comment",
|
|
"pull_request_target",
|
|
"push",
|
|
"registry_package",
|
|
"release",
|
|
"repository_dispatch",
|
|
"schedule",
|
|
"status",
|
|
"watch",
|
|
"workflow_call",
|
|
"workflow_dispatch",
|
|
"workflow_run",
|
|
},
|
|
},
|
|
"status": {
|
|
Type: "string",
|
|
Description: "Returns workflow runs with the check run status",
|
|
Enum: []any{"queued", "in_progress", "completed", "requested", "waiting"},
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo", "workflow_id"},
|
|
}),
|
|
},
|
|
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
|
|
}
|
|
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
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")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
status, err := OptionalParam[string](args, "status")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
|
|
// Get optional pagination parameters
|
|
pagination, err := OptionalPaginationParams(args)
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
|
|
// Set up list options
|
|
opts := &github.ListWorkflowRunsOptions{
|
|
Actor: actor,
|
|
Branch: branch,
|
|
Event: event,
|
|
Status: status,
|
|
ListOptions: github.ListOptions{
|
|
PerPage: pagination.PerPage,
|
|
Page: pagination.Page,
|
|
},
|
|
}
|
|
|
|
workflowRuns, resp, err := client.Actions.ListWorkflowRunsByFileName(ctx, owner, repo, workflowID, opts)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to list workflow runs: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
r, err := json.Marshal(workflowRuns)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// RunWorkflow creates a tool to run an Actions workflow
|
|
func RunWorkflow(t translations.TranslationHelperFunc) inventory.ServerTool {
|
|
return NewTool(
|
|
ToolsetMetadataActions,
|
|
mcp.Tool{
|
|
Name: "run_workflow",
|
|
Description: t("TOOL_RUN_WORKFLOW_DESCRIPTION", "Run an Actions workflow by workflow ID or filename"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_RUN_WORKFLOW_USER_TITLE", "Run workflow"),
|
|
ReadOnlyHint: false,
|
|
},
|
|
InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryOwner,
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryName,
|
|
},
|
|
"workflow_id": {
|
|
Type: "string",
|
|
Description: "The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml)",
|
|
},
|
|
"ref": {
|
|
Type: "string",
|
|
Description: "The git reference for the workflow. The reference can be a branch or tag name.",
|
|
},
|
|
"inputs": {
|
|
Type: "object",
|
|
Description: "Inputs the workflow accepts",
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo", "workflow_id", "ref"},
|
|
},
|
|
},
|
|
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
|
|
}
|
|
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
workflowID, err := RequiredParam[string](args, "workflow_id")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
ref, err := RequiredParam[string](args, "ref")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
|
|
// Get optional inputs parameter
|
|
var inputs map[string]interface{}
|
|
if requestInputs, ok := args["inputs"]; ok {
|
|
if inputsMap, ok := requestInputs.(map[string]interface{}); ok {
|
|
inputs = inputsMap
|
|
}
|
|
}
|
|
|
|
event := github.CreateWorkflowDispatchEventRequest{
|
|
Ref: ref,
|
|
Inputs: inputs,
|
|
}
|
|
|
|
var resp *github.Response
|
|
var workflowType string
|
|
|
|
if workflowIDInt, parseErr := strconv.ParseInt(workflowID, 10, 64); parseErr == nil {
|
|
resp, err = client.Actions.CreateWorkflowDispatchEventByID(ctx, owner, repo, workflowIDInt, event)
|
|
workflowType = "workflow_id"
|
|
} else {
|
|
resp, err = client.Actions.CreateWorkflowDispatchEventByFileName(ctx, owner, repo, workflowID, event)
|
|
workflowType = "workflow_file"
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to run workflow: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
result := map[string]any{
|
|
"message": "Workflow run has been queued",
|
|
"workflow_type": workflowType,
|
|
"workflow_id": workflowID,
|
|
"ref": ref,
|
|
"inputs": inputs,
|
|
"status": resp.Status,
|
|
"status_code": resp.StatusCode,
|
|
}
|
|
|
|
r, err := json.Marshal(result)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// GetWorkflowRun creates a tool to get details of a specific workflow run
|
|
func GetWorkflowRun(t translations.TranslationHelperFunc) inventory.ServerTool {
|
|
return NewTool(
|
|
ToolsetMetadataActions,
|
|
mcp.Tool{
|
|
Name: "get_workflow_run",
|
|
Description: t("TOOL_GET_WORKFLOW_RUN_DESCRIPTION", "Get details of a specific workflow run"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_GET_WORKFLOW_RUN_USER_TITLE", "Get workflow run"),
|
|
ReadOnlyHint: true,
|
|
},
|
|
InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryOwner,
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryName,
|
|
},
|
|
"run_id": {
|
|
Type: "number",
|
|
Description: "The unique identifier of the workflow run",
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo", "run_id"},
|
|
},
|
|
},
|
|
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
|
|
}
|
|
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runIDInt, err := RequiredInt(args, "run_id")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runID := int64(runIDInt)
|
|
|
|
workflowRun, resp, err := client.Actions.GetWorkflowRunByID(ctx, owner, repo, runID)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to get workflow run: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
r, err := json.Marshal(workflowRun)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// GetWorkflowRunLogs creates a tool to download logs for a specific workflow run
|
|
func GetWorkflowRunLogs(t translations.TranslationHelperFunc) inventory.ServerTool {
|
|
return NewTool(
|
|
ToolsetMetadataActions,
|
|
mcp.Tool{
|
|
Name: "get_workflow_run_logs",
|
|
Description: t("TOOL_GET_WORKFLOW_RUN_LOGS_DESCRIPTION", "Download logs for a specific workflow run (EXPENSIVE: downloads ALL logs as ZIP. Consider using get_job_logs with failed_only=true for debugging failed jobs)"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_GET_WORKFLOW_RUN_LOGS_USER_TITLE", "Get workflow run logs"),
|
|
ReadOnlyHint: true,
|
|
},
|
|
InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryOwner,
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryName,
|
|
},
|
|
"run_id": {
|
|
Type: "number",
|
|
Description: "The unique identifier of the workflow run",
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo", "run_id"},
|
|
},
|
|
},
|
|
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
|
|
}
|
|
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runIDInt, err := RequiredInt(args, "run_id")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runID := int64(runIDInt)
|
|
|
|
// Get the download URL for the logs
|
|
url, resp, err := client.Actions.GetWorkflowRunLogs(ctx, owner, repo, runID, 1)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to get workflow run logs: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// Create response with the logs URL and information
|
|
result := map[string]any{
|
|
"logs_url": url.String(),
|
|
"message": "Workflow run logs are available for download",
|
|
"note": "The logs_url provides a download link for the complete workflow run logs as a ZIP archive. You can download this archive to extract and examine individual job logs.",
|
|
"warning": "This downloads ALL logs as a ZIP file which can be large and expensive. For debugging failed jobs, consider using get_job_logs with failed_only=true and run_id instead.",
|
|
"optimization_tip": "Use: get_job_logs with parameters {run_id: " + fmt.Sprintf("%d", runID) + ", failed_only: true} for more efficient failed job debugging",
|
|
}
|
|
|
|
r, err := json.Marshal(result)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// ListWorkflowJobs creates a tool to list jobs for a specific workflow run
|
|
func ListWorkflowJobs(t translations.TranslationHelperFunc) inventory.ServerTool {
|
|
return NewTool(
|
|
ToolsetMetadataActions,
|
|
mcp.Tool{
|
|
Name: "list_workflow_jobs",
|
|
Description: t("TOOL_LIST_WORKFLOW_JOBS_DESCRIPTION", "List jobs for a specific workflow run"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_LIST_WORKFLOW_JOBS_USER_TITLE", "List workflow jobs"),
|
|
ReadOnlyHint: true,
|
|
},
|
|
InputSchema: WithPagination(&jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryOwner,
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryName,
|
|
},
|
|
"run_id": {
|
|
Type: "number",
|
|
Description: "The unique identifier of the workflow run",
|
|
},
|
|
"filter": {
|
|
Type: "string",
|
|
Description: "Filters jobs by their completed_at timestamp",
|
|
Enum: []any{"latest", "all"},
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo", "run_id"},
|
|
}),
|
|
},
|
|
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
|
|
}
|
|
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runIDInt, err := RequiredInt(args, "run_id")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runID := int64(runIDInt)
|
|
|
|
// Get optional filtering parameters
|
|
filter, err := OptionalParam[string](args, "filter")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
|
|
// Get optional pagination parameters
|
|
pagination, err := OptionalPaginationParams(args)
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
|
|
// Set up list options
|
|
opts := &github.ListWorkflowJobsOptions{
|
|
Filter: filter,
|
|
ListOptions: github.ListOptions{
|
|
PerPage: pagination.PerPage,
|
|
Page: pagination.Page,
|
|
},
|
|
}
|
|
|
|
jobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, opts)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to list workflow jobs: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// Add optimization tip for failed job debugging
|
|
response := map[string]any{
|
|
"jobs": jobs,
|
|
"optimization_tip": "For debugging failed jobs, consider using get_job_logs with failed_only=true and run_id=" + fmt.Sprintf("%d", runID) + " to get logs directly without needing to list jobs first",
|
|
}
|
|
|
|
r, err := json.Marshal(response)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// GetJobLogs creates a tool to download logs for a specific workflow job or efficiently get all failed job logs for a workflow run
|
|
func GetJobLogs(t translations.TranslationHelperFunc) inventory.ServerTool {
|
|
return NewTool(
|
|
ToolsetMetadataActions,
|
|
mcp.Tool{
|
|
Name: "get_job_logs",
|
|
Description: t("TOOL_GET_JOB_LOGS_DESCRIPTION", "Download logs for a specific workflow job or efficiently get all failed job logs for a workflow run"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_GET_JOB_LOGS_USER_TITLE", "Get job logs"),
|
|
ReadOnlyHint: true,
|
|
},
|
|
InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryOwner,
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryName,
|
|
},
|
|
"job_id": {
|
|
Type: "number",
|
|
Description: "The unique identifier of the workflow job (required for single job logs)",
|
|
},
|
|
"run_id": {
|
|
Type: "number",
|
|
Description: "Workflow run ID (required when using failed_only)",
|
|
},
|
|
"failed_only": {
|
|
Type: "boolean",
|
|
Description: "When true, gets logs for all failed jobs in run_id",
|
|
},
|
|
"return_content": {
|
|
Type: "boolean",
|
|
Description: "Returns actual log content instead of URLs",
|
|
},
|
|
"tail_lines": {
|
|
Type: "number",
|
|
Description: "Number of lines to return from the end of the log",
|
|
Default: json.RawMessage(`500`),
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo"},
|
|
},
|
|
},
|
|
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
|
|
}
|
|
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
|
|
// Get optional parameters
|
|
jobID, err := OptionalIntParam(args, "job_id")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runID, err := OptionalIntParam(args, "run_id")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
failedOnly, err := OptionalParam[bool](args, "failed_only")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
returnContent, err := OptionalParam[bool](args, "return_content")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
tailLines, err := OptionalIntParam(args, "tail_lines")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
// Default to 500 lines if not specified
|
|
if tailLines == 0 {
|
|
tailLines = 500
|
|
}
|
|
|
|
// Validate parameters
|
|
if failedOnly && runID == 0 {
|
|
return utils.NewToolResultError("run_id is required when failed_only is true"), nil, nil
|
|
}
|
|
if !failedOnly && jobID == 0 {
|
|
return utils.NewToolResultError("job_id is required when failed_only is false"), nil, nil
|
|
}
|
|
|
|
if failedOnly && runID > 0 {
|
|
// Handle failed-only mode: get logs for all failed jobs in the workflow run
|
|
return handleFailedJobLogs(ctx, client, owner, repo, int64(runID), returnContent, tailLines, deps.GetContentWindowSize())
|
|
} else if jobID > 0 {
|
|
// Handle single job mode
|
|
return handleSingleJobLogs(ctx, client, owner, repo, int64(jobID), returnContent, tailLines, deps.GetContentWindowSize())
|
|
}
|
|
|
|
return utils.NewToolResultError("Either job_id must be provided for single job logs, or run_id with failed_only=true for failed job logs"), nil, nil
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// handleFailedJobLogs gets logs for all failed jobs in a workflow run
|
|
func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo string, runID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, any, error) {
|
|
// First, get all jobs for the workflow run
|
|
jobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, &github.ListWorkflowJobsOptions{
|
|
Filter: "latest",
|
|
})
|
|
if err != nil {
|
|
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow jobs", resp, err), nil, nil
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// Filter for failed jobs
|
|
var failedJobs []*github.WorkflowJob
|
|
for _, job := range jobs.Jobs {
|
|
if job.GetConclusion() == "failure" {
|
|
failedJobs = append(failedJobs, job)
|
|
}
|
|
}
|
|
|
|
if len(failedJobs) == 0 {
|
|
result := map[string]any{
|
|
"message": "No failed jobs found in this workflow run",
|
|
"run_id": runID,
|
|
"total_jobs": len(jobs.Jobs),
|
|
"failed_jobs": 0,
|
|
}
|
|
r, _ := json.Marshal(result)
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
}
|
|
|
|
// Collect logs for all failed jobs
|
|
var logResults []map[string]any
|
|
for _, job := range failedJobs {
|
|
jobResult, resp, err := getJobLogData(ctx, client, owner, repo, job.GetID(), job.GetName(), returnContent, tailLines, contentWindowSize)
|
|
if err != nil {
|
|
// Continue with other jobs even if one fails
|
|
jobResult = map[string]any{
|
|
"job_id": job.GetID(),
|
|
"job_name": job.GetName(),
|
|
"error": err.Error(),
|
|
}
|
|
// Enable reporting of status codes and error causes
|
|
_, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get job logs", resp, err) // Explicitly ignore error for graceful handling
|
|
}
|
|
|
|
logResults = append(logResults, jobResult)
|
|
}
|
|
|
|
result := map[string]any{
|
|
"message": fmt.Sprintf("Retrieved logs for %d failed jobs", len(failedJobs)),
|
|
"run_id": runID,
|
|
"total_jobs": len(jobs.Jobs),
|
|
"failed_jobs": len(failedJobs),
|
|
"logs": logResults,
|
|
"return_format": map[string]bool{"content": returnContent, "urls": !returnContent},
|
|
}
|
|
|
|
r, err := json.Marshal(result)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
}
|
|
|
|
// handleSingleJobLogs gets logs for a single job
|
|
func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo string, jobID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, any, error) {
|
|
jobResult, resp, err := getJobLogData(ctx, client, owner, repo, jobID, "", returnContent, tailLines, contentWindowSize)
|
|
if err != nil {
|
|
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get job logs", resp, err), nil, nil
|
|
}
|
|
|
|
r, err := json.Marshal(jobResult)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
}
|
|
|
|
// getJobLogData retrieves log data for a single job, either as URL or content
|
|
func getJobLogData(ctx context.Context, client *github.Client, owner, repo string, jobID int64, jobName string, returnContent bool, tailLines int, contentWindowSize int) (map[string]any, *github.Response, error) {
|
|
// Get the download URL for the job logs
|
|
url, resp, err := client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 1)
|
|
if err != nil {
|
|
return nil, resp, fmt.Errorf("failed to get job logs for job %d: %w", jobID, err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
result := map[string]any{
|
|
"job_id": jobID,
|
|
}
|
|
if jobName != "" {
|
|
result["job_name"] = jobName
|
|
}
|
|
|
|
if returnContent {
|
|
// Download and return the actual log content
|
|
content, originalLength, httpResp, err := downloadLogContent(ctx, url.String(), tailLines, contentWindowSize) //nolint:bodyclose // Response body is closed in downloadLogContent, but we need to return httpResp
|
|
if err != nil {
|
|
// To keep the return value consistent wrap the response as a GitHub Response
|
|
ghRes := &github.Response{
|
|
Response: httpResp,
|
|
}
|
|
return nil, ghRes, fmt.Errorf("failed to download log content for job %d: %w", jobID, err)
|
|
}
|
|
result["logs_content"] = content
|
|
result["message"] = "Job logs content retrieved successfully"
|
|
result["original_length"] = originalLength
|
|
} else {
|
|
// Return just the URL
|
|
result["logs_url"] = url.String()
|
|
result["message"] = "Job logs are available for download"
|
|
result["note"] = "The logs_url provides a download link for the individual job logs in plain text format. Use return_content=true to get the actual log content."
|
|
}
|
|
|
|
return result, resp, nil
|
|
}
|
|
|
|
func downloadLogContent(ctx context.Context, logURL string, tailLines int, maxLines int) (string, int, *http.Response, error) {
|
|
prof := profiler.New(nil, profiler.IsProfilingEnabled())
|
|
finish := prof.Start(ctx, "log_buffer_processing")
|
|
|
|
httpResp, err := http.Get(logURL) //nolint:gosec
|
|
if err != nil {
|
|
return "", 0, httpResp, fmt.Errorf("failed to download logs: %w", err)
|
|
}
|
|
defer func() { _ = httpResp.Body.Close() }()
|
|
|
|
if httpResp.StatusCode != http.StatusOK {
|
|
return "", 0, httpResp, fmt.Errorf("failed to download logs: HTTP %d", httpResp.StatusCode)
|
|
}
|
|
|
|
bufferSize := tailLines
|
|
if bufferSize > maxLines {
|
|
bufferSize = maxLines
|
|
}
|
|
|
|
processedInput, totalLines, httpResp, err := buffer.ProcessResponseAsRingBufferToEnd(httpResp, bufferSize)
|
|
if err != nil {
|
|
return "", 0, httpResp, fmt.Errorf("failed to process log content: %w", err)
|
|
}
|
|
|
|
lines := strings.Split(processedInput, "\n")
|
|
if len(lines) > tailLines {
|
|
lines = lines[len(lines)-tailLines:]
|
|
}
|
|
finalResult := strings.Join(lines, "\n")
|
|
|
|
_ = finish(len(lines), int64(len(finalResult)))
|
|
|
|
return finalResult, totalLines, httpResp, nil
|
|
}
|
|
|
|
// RerunWorkflowRun creates a tool to re-run an entire workflow run
|
|
func RerunWorkflowRun(t translations.TranslationHelperFunc) inventory.ServerTool {
|
|
return NewTool(
|
|
ToolsetMetadataActions,
|
|
mcp.Tool{
|
|
Name: "rerun_workflow_run",
|
|
Description: t("TOOL_RERUN_WORKFLOW_RUN_DESCRIPTION", "Re-run an entire workflow run"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_RERUN_WORKFLOW_RUN_USER_TITLE", "Rerun workflow run"),
|
|
ReadOnlyHint: false,
|
|
},
|
|
InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryOwner,
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryName,
|
|
},
|
|
"run_id": {
|
|
Type: "number",
|
|
Description: "The unique identifier of the workflow run",
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo", "run_id"},
|
|
},
|
|
},
|
|
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
|
|
}
|
|
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runIDInt, err := RequiredInt(args, "run_id")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runID := int64(runIDInt)
|
|
|
|
resp, err := client.Actions.RerunWorkflowByID(ctx, owner, repo, runID)
|
|
if err != nil {
|
|
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to rerun workflow run", resp, err), nil, nil
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
result := map[string]any{
|
|
"message": "Workflow run has been queued for re-run",
|
|
"run_id": runID,
|
|
"status": resp.Status,
|
|
"status_code": resp.StatusCode,
|
|
}
|
|
|
|
r, err := json.Marshal(result)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// RerunFailedJobs creates a tool to re-run only the failed jobs in a workflow run
|
|
func RerunFailedJobs(t translations.TranslationHelperFunc) inventory.ServerTool {
|
|
return NewTool(
|
|
ToolsetMetadataActions,
|
|
mcp.Tool{
|
|
Name: "rerun_failed_jobs",
|
|
Description: t("TOOL_RERUN_FAILED_JOBS_DESCRIPTION", "Re-run only the failed jobs in a workflow run"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_RERUN_FAILED_JOBS_USER_TITLE", "Rerun failed jobs"),
|
|
ReadOnlyHint: false,
|
|
},
|
|
InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryOwner,
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryName,
|
|
},
|
|
"run_id": {
|
|
Type: "number",
|
|
Description: "The unique identifier of the workflow run",
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo", "run_id"},
|
|
},
|
|
},
|
|
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
|
|
}
|
|
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runIDInt, err := RequiredInt(args, "run_id")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runID := int64(runIDInt)
|
|
|
|
resp, err := client.Actions.RerunFailedJobsByID(ctx, owner, repo, runID)
|
|
if err != nil {
|
|
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to rerun failed jobs", resp, err), nil, nil
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
result := map[string]any{
|
|
"message": "Failed jobs have been queued for re-run",
|
|
"run_id": runID,
|
|
"status": resp.Status,
|
|
"status_code": resp.StatusCode,
|
|
}
|
|
|
|
r, err := json.Marshal(result)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// CancelWorkflowRun creates a tool to cancel a workflow run
|
|
func CancelWorkflowRun(t translations.TranslationHelperFunc) inventory.ServerTool {
|
|
return NewTool(
|
|
ToolsetMetadataActions,
|
|
mcp.Tool{
|
|
Name: "cancel_workflow_run",
|
|
Description: t("TOOL_CANCEL_WORKFLOW_RUN_DESCRIPTION", "Cancel a workflow run"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_CANCEL_WORKFLOW_RUN_USER_TITLE", "Cancel workflow run"),
|
|
ReadOnlyHint: false,
|
|
},
|
|
InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryOwner,
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryName,
|
|
},
|
|
"run_id": {
|
|
Type: "number",
|
|
Description: "The unique identifier of the workflow run",
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo", "run_id"},
|
|
},
|
|
},
|
|
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
|
|
}
|
|
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runIDInt, err := RequiredInt(args, "run_id")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runID := int64(runIDInt)
|
|
|
|
resp, err := client.Actions.CancelWorkflowRunByID(ctx, owner, repo, runID)
|
|
if err != nil {
|
|
if _, ok := err.(*github.AcceptedError); !ok {
|
|
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to cancel workflow run", resp, err), nil, nil
|
|
}
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
result := map[string]any{
|
|
"message": "Workflow run has been cancelled",
|
|
"run_id": runID,
|
|
"status": resp.Status,
|
|
"status_code": resp.StatusCode,
|
|
}
|
|
|
|
r, err := json.Marshal(result)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// ListWorkflowRunArtifacts creates a tool to list artifacts for a workflow run
|
|
func ListWorkflowRunArtifacts(t translations.TranslationHelperFunc) inventory.ServerTool {
|
|
return NewTool(
|
|
ToolsetMetadataActions,
|
|
mcp.Tool{
|
|
Name: "list_workflow_run_artifacts",
|
|
Description: t("TOOL_LIST_WORKFLOW_RUN_ARTIFACTS_DESCRIPTION", "List artifacts for a workflow run"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_LIST_WORKFLOW_RUN_ARTIFACTS_USER_TITLE", "List workflow artifacts"),
|
|
ReadOnlyHint: true,
|
|
},
|
|
InputSchema: WithPagination(&jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryOwner,
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryName,
|
|
},
|
|
"run_id": {
|
|
Type: "number",
|
|
Description: "The unique identifier of the workflow run",
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo", "run_id"},
|
|
}),
|
|
},
|
|
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
|
|
}
|
|
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runIDInt, err := RequiredInt(args, "run_id")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runID := int64(runIDInt)
|
|
|
|
// Get optional pagination parameters
|
|
pagination, err := OptionalPaginationParams(args)
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
|
|
// Set up list options
|
|
opts := &github.ListOptions{
|
|
PerPage: pagination.PerPage,
|
|
Page: pagination.Page,
|
|
}
|
|
|
|
artifacts, resp, err := client.Actions.ListWorkflowRunArtifacts(ctx, owner, repo, runID, opts)
|
|
if err != nil {
|
|
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow run artifacts", resp, err), nil, nil
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
r, err := json.Marshal(artifacts)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// DownloadWorkflowRunArtifact creates a tool to download a workflow run artifact
|
|
func DownloadWorkflowRunArtifact(t translations.TranslationHelperFunc) inventory.ServerTool {
|
|
return NewTool(
|
|
ToolsetMetadataActions,
|
|
mcp.Tool{
|
|
Name: "download_workflow_run_artifact",
|
|
Description: t("TOOL_DOWNLOAD_WORKFLOW_RUN_ARTIFACT_DESCRIPTION", "Get download URL for a workflow run artifact"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_DOWNLOAD_WORKFLOW_RUN_ARTIFACT_USER_TITLE", "Download workflow artifact"),
|
|
ReadOnlyHint: true,
|
|
},
|
|
InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryOwner,
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryName,
|
|
},
|
|
"artifact_id": {
|
|
Type: "number",
|
|
Description: "The unique identifier of the artifact",
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo", "artifact_id"},
|
|
},
|
|
},
|
|
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
|
|
}
|
|
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
artifactIDInt, err := RequiredInt(args, "artifact_id")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
artifactID := int64(artifactIDInt)
|
|
|
|
// Get the download URL for the artifact
|
|
url, resp, err := client.Actions.DownloadArtifact(ctx, owner, repo, artifactID, 1)
|
|
if err != nil {
|
|
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get artifact download URL", resp, err), nil, nil
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// Create response with the download URL and information
|
|
result := map[string]any{
|
|
"download_url": url.String(),
|
|
"message": "Artifact is available for download",
|
|
"note": "The download_url provides a download link for the artifact as a ZIP archive. The link is temporary and expires after a short time.",
|
|
"artifact_id": artifactID,
|
|
}
|
|
|
|
r, err := json.Marshal(result)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// DeleteWorkflowRunLogs creates a tool to delete logs for a workflow run
|
|
func DeleteWorkflowRunLogs(t translations.TranslationHelperFunc) inventory.ServerTool {
|
|
return NewTool(
|
|
ToolsetMetadataActions,
|
|
mcp.Tool{
|
|
Name: "delete_workflow_run_logs",
|
|
Description: t("TOOL_DELETE_WORKFLOW_RUN_LOGS_DESCRIPTION", "Delete logs for a workflow run"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_DELETE_WORKFLOW_RUN_LOGS_USER_TITLE", "Delete workflow logs"),
|
|
ReadOnlyHint: false,
|
|
DestructiveHint: jsonschema.Ptr(true),
|
|
},
|
|
InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryOwner,
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryName,
|
|
},
|
|
"run_id": {
|
|
Type: "number",
|
|
Description: "The unique identifier of the workflow run",
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo", "run_id"},
|
|
},
|
|
},
|
|
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
|
|
}
|
|
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runIDInt, err := RequiredInt(args, "run_id")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runID := int64(runIDInt)
|
|
|
|
resp, err := client.Actions.DeleteWorkflowRunLogs(ctx, owner, repo, runID)
|
|
if err != nil {
|
|
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to delete workflow run logs", resp, err), nil, nil
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
result := map[string]any{
|
|
"message": "Workflow run logs have been deleted",
|
|
"run_id": runID,
|
|
"status": resp.Status,
|
|
"status_code": resp.StatusCode,
|
|
}
|
|
|
|
r, err := json.Marshal(result)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return utils.NewToolResultText(string(r)), nil, nil
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// GetWorkflowRunUsage creates a tool to get usage metrics for a workflow run
|
|
func GetWorkflowRunUsage(t translations.TranslationHelperFunc) inventory.ServerTool {
|
|
return NewTool(
|
|
ToolsetMetadataActions,
|
|
mcp.Tool{
|
|
Name: "get_workflow_run_usage",
|
|
Description: t("TOOL_GET_WORKFLOW_RUN_USAGE_DESCRIPTION", "Get usage metrics for a workflow run"),
|
|
Annotations: &mcp.ToolAnnotations{
|
|
Title: t("TOOL_GET_WORKFLOW_RUN_USAGE_USER_TITLE", "Get workflow usage"),
|
|
ReadOnlyHint: true,
|
|
},
|
|
InputSchema: &jsonschema.Schema{
|
|
Type: "object",
|
|
Properties: map[string]*jsonschema.Schema{
|
|
"owner": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryOwner,
|
|
},
|
|
"repo": {
|
|
Type: "string",
|
|
Description: DescriptionRepositoryName,
|
|
},
|
|
"run_id": {
|
|
Type: "number",
|
|
Description: "The unique identifier of the workflow run",
|
|
},
|
|
},
|
|
Required: []string{"owner", "repo", "run_id"},
|
|
},
|
|
},
|
|
func(deps ToolDependencies) mcp.ToolHandlerFor[map[string]any, any] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
|
|
client, err := deps.GetClient(ctx)
|
|
if err != nil {
|
|
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
|
|
}
|
|
|
|
owner, err := RequiredParam[string](args, "owner")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
repo, err := RequiredParam[string](args, "repo")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runIDInt, err := RequiredInt(args, "run_id")
|
|
if err != nil {
|
|
return utils.NewToolResultError(err.Error()), nil, nil
|
|
}
|
|
runID := int64(runIDInt)
|
|
|
|
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
|
|
}
|
|
},
|
|
)
|
|
}
|