342 lines
10 KiB
Go
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
|
|
}
|