Files
gitea-mcp-server/mcp/operation/actions/runners.go
T

194 lines
5.4 KiB
Go

package actions
import (
"context"
"fmt"
"net/http"
"net/url"
"gitea.com/gitea/gitea-mcp/pkg/errors"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListActionRunnersToolName = "list_action_runners"
)
// ActionRunner represents a self-hosted action runner
// This is a local type since Gitea SDK v0.23.2 doesn't include it
type ActionRunner struct {
ID int64 `json:"id"`
Name string `json:"name"`
UUID string `json:"uuid"`
Status string `json:"status"`
Online bool `json:"online"`
Busy bool `json:"busy"`
Version string `json:"version,omitempty"`
Labels []string `json:"labels,omitempty"`
LastOnline string `json:"last_online,omitempty"`
}
// ActionRunnersResponse represents the API response for listing runners
type ActionRunnersResponse struct {
TotalCount int `json:"total_count"`
Runners []*ActionRunner `json:"runners"`
}
var (
ListActionRunnersTool = mcp.NewTool(
ListActionRunnersToolName,
mcp.WithDescription("List self-hosted action runners for a repository. Shows runner status, labels, and availability. Filter by status (online/offline). Note: Requires Gitea 1.23+; Gitea 1.22.5 does not support Actions API."),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("status", mcp.Description("optional status filter (online, offline, busy, idle)"), mcp.Enum("online", "offline", "busy", "idle")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30), mcp.Min(1)),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{
Tool: ListActionRunnersTool,
Handler: listActionRunnersFn,
})
}
func listActionRunnersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called listActionRunnersFn")
owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil {
return to.ErrorResult(errors.TranslateError(err, map[string]string{
"operation": "ListActionRunners",
"param": "owner",
}))
}
repo, err := params.GetString(req.GetArguments(), "repo")
if err != nil {
return to.ErrorResult(errors.TranslateError(err, map[string]string{
"operation": "ListActionRunners",
"param": "repo",
}))
}
statusFilter, _ := req.GetArguments()["status"].(string)
page, pageSize := params.GetPagination(req.GetArguments(), 30)
// Use REST API to get runners
apiPath := fmt.Sprintf("/repos/%s/%s/actions/runners", owner, repo)
query := url.Values{
"page": []string{fmt.Sprintf("%d", page)},
"per_page": []string{fmt.Sprintf("%d", pageSize)},
}
var runnersResp ActionRunnersResponse
statusCode, err := gitea.DoJSON(ctx, http.MethodGet, apiPath, query, nil, &runnersResp)
if err != nil {
if statusCode == http.StatusNotFound {
// Gitea 1.22.5 doesn't have Actions API - return empty list with message
return to.TextResult(map[string]interface{}{
"total_count": 0,
"runners": []interface{}{},
"note": "Actions API not available in Gitea 1.22.5. Requires Gitea 1.23+.",
})
}
return to.ErrorResult(errors.TranslateError(err, map[string]string{
"operation": "ListActionRunners",
"owner": owner,
"repo": repo,
}))
}
// Filter by status if requested
filteredRunners := make([]*ActionRunner, 0, len(runnersResp.Runners))
if statusFilter != "" {
for _, runner := range runnersResp.Runners {
switch statusFilter {
case "online":
if runner.Online {
filteredRunners = append(filteredRunners, runner)
}
case "offline":
if !runner.Online {
filteredRunners = append(filteredRunners, runner)
}
case "busy":
if runner.Busy {
filteredRunners = append(filteredRunners, runner)
}
case "idle":
if runner.Online && !runner.Busy {
filteredRunners = append(filteredRunners, runner)
}
default:
if runner.Status == statusFilter {
filteredRunners = append(filteredRunners, runner)
}
}
}
} else {
filteredRunners = runnersResp.Runners
}
result := slimActionRunners(filteredRunners)
return to.TextResult(result)
}
func slimActionRunners(runners []*ActionRunner) map[string]interface{} {
if len(runners) == 0 {
return map[string]interface{}{
"total_count": 0,
"runners": []interface{}{},
}
}
slimmed := make([]map[string]interface{}, 0, len(runners))
for _, runner := range runners {
slimmed = append(slimmed, slimActionRunner(runner))
}
return map[string]interface{}{
"total_count": len(runners),
"runners": slimmed,
}
}
func slimActionRunner(runner *ActionRunner) map[string]interface{} {
if runner == nil {
return nil
}
result := map[string]interface{}{
"id": runner.ID,
"name": runner.Name,
"uuid": runner.UUID,
"status": runner.Status,
"online": runner.Online,
"busy": runner.Busy,
}
if runner.Version != "" {
result["version"] = runner.Version
}
if len(runner.Labels) > 0 {
result["labels"] = runner.Labels
} else {
result["labels"] = []string{}
}
if runner.LastOnline != "" {
result["last_online"] = runner.LastOnline
}
return result
}