woodpecker-mcp/internal/tools/pipelines.go
2026-08-08 01:37:47 +03:00

342 lines
10 KiB
Go

package tools
import (
"context"
"fmt"
"net/url"
"github.com/mark3labs/mcp-go/mcp"
mcpgo "github.com/mark3labs/mcp-go/server"
"gitea.fe.kaien.ru/kaien/woodpecker-mcp/internal/woodpecker"
)
// pipelineSummaryOutput is the compact client-facing shape used by
// list_pipelines.
type pipelineSummaryOutput struct {
ID int64 `json:"id"`
Number int64 `json:"number"`
Status string `json:"status"`
Event string `json:"event"`
Branch string `json:"branch"`
Commit string `json:"commit"`
Message *string `json:"message,omitempty"`
Author *string `json:"author,omitempty"`
Started *int64 `json:"started,omitempty"`
Finished *int64 `json:"finished,omitempty"`
Created int64 `json:"created"`
Updated int64 `json:"updated"`
}
func toPipelineSummaryOutput(p woodpecker.Pipeline) pipelineSummaryOutput {
return pipelineSummaryOutput{
ID: p.ID,
Number: p.Number,
Status: p.Status,
Event: p.Event,
Branch: p.Branch,
Commit: p.Commit,
Message: p.Message,
Author: p.Author,
Started: p.StartedAt,
Finished: p.FinishedAt,
Created: p.CreatedAt,
Updated: p.UpdatedAt,
}
}
// stepOutput is a step inside a workflow of the pipeline detail view.
type stepOutput struct {
ID int64 `json:"id"`
Name string `json:"name"`
State string `json:"state"`
Error *string `json:"error,omitempty"`
ExitCode *int64 `json:"exitCode,omitempty"`
Started *int64 `json:"started,omitempty"`
Finished *int64 `json:"finished,omitempty"`
}
// workflowOutput is a workflow of the pipeline detail view.
type workflowOutput struct {
ID int64 `json:"id"`
Name string `json:"name"`
State string `json:"state"`
Error *string `json:"error,omitempty"`
Started *int64 `json:"started,omitempty"`
Finished *int64 `json:"finished,omitempty"`
Children []stepOutput `json:"children,omitempty"`
}
// pipelineOutput is the full client-facing pipeline shape used by get_pipeline.
type pipelineOutput struct {
ID int64 `json:"id"`
Number int64 `json:"number"`
Status string `json:"status"`
Event string `json:"event"`
Branch string `json:"branch"`
Commit string `json:"commit"`
Message *string `json:"message,omitempty"`
Author *string `json:"author,omitempty"`
Email *string `json:"email,omitempty"`
Started *int64 `json:"started,omitempty"`
Finished *int64 `json:"finished,omitempty"`
Created int64 `json:"created"`
Updated int64 `json:"updated"`
Workflows []workflowOutput `json:"workflows,omitempty"`
}
func toPipelineOutput(p woodpecker.Pipeline) pipelineOutput {
out := pipelineOutput{
ID: p.ID,
Number: p.Number,
Status: p.Status,
Event: p.Event,
Branch: p.Branch,
Commit: p.Commit,
Message: p.Message,
Author: p.Author,
Email: p.AuthorEmail,
Started: p.StartedAt,
Finished: p.FinishedAt,
Created: p.CreatedAt,
Updated: p.UpdatedAt,
}
for _, w := range p.Workflows {
wo := workflowOutput{
ID: w.ID,
Name: w.Name,
State: w.State,
Error: w.Error,
Started: w.StartTime,
Finished: w.EndTime,
}
for _, st := range w.Children {
wo.Children = append(wo.Children, stepOutput{
ID: st.ID,
Name: st.Name,
State: st.State,
Error: st.Error,
ExitCode: st.ExitCode,
Started: st.StartTime,
Finished: st.EndTime,
})
}
out.Workflows = append(out.Workflows, wo)
}
return out
}
// registerPipelines registers the pipeline tools: list_pipelines, get_pipeline
// and get_pipeline_config.
func registerPipelines(s *mcpgo.MCPServer, c *woodpecker.Client) {
tool, handler := listPipelinesTool(c)
s.AddTool(tool, handler)
tool, handler = getPipelineTool(c)
s.AddTool(tool, handler)
tool, handler = getPipelineConfigTool(c)
s.AddTool(tool, handler)
}
// parseRepoID reads and validates the repoId argument.
func parseRepoID(args map[string]any) (int64, *mcp.CallToolResult) {
repoID, ok := argInt64(args, "repoId")
if !ok || repoID < 1 {
return 0, mcp.NewToolResultError(render(map[string]string{
"error": "repoId must be a positive integer",
}))
}
return repoID, nil
}
// parsePipelineNumber reads and validates the number argument.
func parsePipelineNumber(args map[string]any) (int64, *mcp.CallToolResult) {
number, ok := argInt64(args, "number")
if !ok || number < 1 {
return 0, mcp.NewToolResultError(render(map[string]string{
"error": "number must be a positive integer",
}))
}
return number, nil
}
// listPipelinesTool lists pipelines for a repository with optional filtering.
func listPipelinesTool(c *woodpecker.Client) (mcp.Tool, mcpgo.ToolHandlerFunc) {
tool := newReadOnlyTool("list_pipelines",
mcp.WithToolTitle("List Pipelines"),
mcp.WithDescription(`List pipelines for a repository with optional filtering. Returns pipeline metadata including status, commit info, and timing.
If required information is not provided, use the git command or any available tool to get more context about the current commit, the repository, or the author of the commit.`),
mcp.WithInteger("repoId",
mcp.Required(),
mcp.Description("Numeric repository ID"),
mcp.Min(1),
),
mcp.WithInteger("page",
mcp.Description("Page number for pagination"),
mcp.Min(1),
mcp.DefaultNumber(defaultPage),
),
mcp.WithInteger("perPage",
mcp.Description("Number of pipelines per page"),
mcp.Min(1),
mcp.Max(maxPerPage),
mcp.DefaultNumber(defaultPerPage),
),
mcp.WithString("before",
mcp.Description("Only return pipelines before this RFC3339 date (e.g., '2023-12-01T10:00:00Z')"),
),
mcp.WithString("after",
mcp.Description("Only return pipelines after this RFC3339 date (e.g., '2023-12-01T10:00:00Z')"),
),
)
handler := func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := decodedArguments(req)
repoID, bad := parseRepoID(args)
if bad != nil {
return bad, nil
}
page, err := parseIntParam(args, "page", defaultPage, 1, 1<<31-1)
if err != nil {
return mcp.NewToolResultError(render(map[string]string{"error": err.Error()})), nil
}
perPage, err := parseIntParam(args, "perPage", defaultPerPage, 1, maxPerPage)
if err != nil {
return mcp.NewToolResultError(render(map[string]string{"error": err.Error()})), nil
}
q := url.Values{}
q.Set("page", formatInt64(page))
q.Set("perPage", formatInt64(perPage))
if before := argString(args, "before"); before != "" {
q.Set("before", before)
}
if after := argString(args, "after"); after != "" {
q.Set("after", after)
}
var pipelines []woodpecker.Pipeline
endpoint := fmt.Sprintf("/repos/%d/pipelines?%s", repoID, q.Encode())
if err := c.Get(ctx, endpoint, &pipelines); err != nil {
return renderError(err, fmt.Sprintf("pipelines for repository %d", repoID)), nil
}
output := make([]pipelineSummaryOutput, 0, len(pipelines))
for _, p := range pipelines {
output = append(output, toPipelineSummaryOutput(p))
}
return mcp.NewToolResultText(render(map[string]any{
"repoId": repoID,
"pipelines": output,
"count": len(output),
})), nil
}
return tool, handler
}
// getPipelineTool returns detailed information about a specific pipeline.
func getPipelineTool(c *woodpecker.Client) (mcp.Tool, mcpgo.ToolHandlerFunc) {
tool := newReadOnlyTool("get_pipeline",
mcp.WithToolTitle("Get Pipeline"),
mcp.WithDescription(`Get detailed information about a specific pipeline, including workflows, steps, and error details.
If required information is not provided, use the git command or any available tool to get more context about the current commit, the repository, or the author of the commit.`),
mcp.WithInteger("repoId",
mcp.Required(),
mcp.Description("The repository ID"),
mcp.Min(1),
),
mcp.WithInteger("number",
mcp.Required(),
mcp.Description("The pipeline number"),
mcp.Min(1),
),
)
handler := func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := decodedArguments(req)
repoID, bad := parseRepoID(args)
if bad != nil {
return bad, nil
}
number, bad := parsePipelineNumber(args)
if bad != nil {
return bad, nil
}
var pipeline woodpecker.Pipeline
endpoint := fmt.Sprintf("/repos/%d/pipelines/%d", repoID, number)
if err := c.Get(ctx, endpoint, &pipeline); err != nil {
return renderError(err, fmt.Sprintf("pipeline %d in repository %d", number, repoID)), nil
}
return mcp.NewToolResultText(render(map[string]any{
"pipelineNumber": number,
"pipeline": toPipelineOutput(pipeline),
})), nil
}
return tool, handler
}
// configOutput is a decoded pipeline configuration file.
type configOutput struct {
Name string `json:"name"`
Hash string `json:"hash"`
Content string `json:"content"`
}
// getPipelineConfigTool returns the configuration files used by a pipeline.
func getPipelineConfigTool(c *woodpecker.Client) (mcp.Tool, mcpgo.ToolHandlerFunc) {
tool := newReadOnlyTool("get_pipeline_config",
mcp.WithToolTitle("Get Pipeline Config"),
mcp.WithDescription(`Get the configuration files (.woodpecker.yml) used by a specific pipeline. Essential understanding pipeline setup.
If required information is not provided, use the git command or any available tool to get more context about the current commit, the repository, or the author of the commit.`),
mcp.WithInteger("repoId",
mcp.Required(),
mcp.Description("The repository ID"),
mcp.Min(1),
),
mcp.WithInteger("number",
mcp.Required(),
mcp.Description("The pipeline number"),
mcp.Min(1),
),
)
handler := func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := decodedArguments(req)
repoID, bad := parseRepoID(args)
if bad != nil {
return bad, nil
}
number, bad := parsePipelineNumber(args)
if bad != nil {
return bad, nil
}
var configs []woodpecker.Config
endpoint := fmt.Sprintf("/repos/%d/pipelines/%d/config", repoID, number)
if err := c.Get(ctx, endpoint, &configs); err != nil {
return renderError(err, fmt.Sprintf("configuration for pipeline %d in repository %d", number, repoID)), nil
}
output := make([]configOutput, 0, len(configs))
for _, cfg := range configs {
output = append(output, configOutput{
Name: cfg.Name,
Hash: cfg.Hash,
Content: decodeBase64(cfg.Data),
})
}
return mcp.NewToolResultText(render(map[string]any{
"pipelineNumber": number,
"configs": output,
"count": len(output),
})), nil
}
return tool, handler
}