103 lines
3.0 KiB
Go
103 lines
3.0 KiB
Go
package tools
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
mcpgo "github.com/mark3labs/mcp-go/server"
|
|
|
|
"gitea.fe.kaien.ru/kaien/woodpecker-mcp/internal/woodpecker"
|
|
)
|
|
|
|
// noLogsMessage mirrors the exact message the original client returns when a
|
|
// step has no logs.
|
|
const noLogsMessage = "No logs available for this step -- use get_pipeline to verify step existence or status"
|
|
|
|
// registerLogs registers the log tools: get_step_logs.
|
|
func registerLogs(s *mcpgo.MCPServer, c *woodpecker.Client) {
|
|
tool, handler := getStepLogsTool(c)
|
|
s.AddTool(tool, handler)
|
|
}
|
|
|
|
// getStepLogsTool returns the decoded logs of a specific pipeline step.
|
|
func getStepLogsTool(c *woodpecker.Client) (mcp.Tool, mcpgo.ToolHandlerFunc) {
|
|
tool := newReadOnlyTool("get_step_logs",
|
|
mcp.WithToolTitle("Get Step Logs"),
|
|
mcp.WithDescription("Get logs for a specific pipeline step. Useful for debugging individual step failures or examining step output."),
|
|
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),
|
|
),
|
|
mcp.WithInteger("stepId",
|
|
mcp.Required(),
|
|
mcp.Description("The step ID (can be found in pipeline details)"),
|
|
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
|
|
}
|
|
stepID, ok := argInt64(args, "stepId")
|
|
if !ok || stepID < 1 {
|
|
return mcp.NewToolResultError(render(map[string]string{
|
|
"error": "stepId must be a positive integer",
|
|
})), nil
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("/repos/%d/logs/%d/%d", repoID, number, stepID)
|
|
|
|
var raw json.RawMessage
|
|
if err := c.Get(ctx, endpoint, &raw); err != nil {
|
|
return renderError(err, fmt.Sprintf("logs for step %d in pipeline %d of repository %d", stepID, number, repoID)), nil
|
|
}
|
|
|
|
// A literal null (step never ran) yields the no-logs message; an
|
|
// empty array yields an empty log string, mirroring the original.
|
|
if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
|
return mcp.NewToolResultText(render(map[string]any{
|
|
"stepId": stepID,
|
|
"logs": noLogsMessage,
|
|
})), nil
|
|
}
|
|
|
|
var logs []woodpecker.LogEntry
|
|
if err := json.Unmarshal(raw, &logs); err != nil {
|
|
return renderError(errors.New("failed to decode logs: "+err.Error()), fmt.Sprintf("logs for step %d in pipeline %d of repository %d", stepID, number, repoID)), nil
|
|
}
|
|
|
|
lines := make([]string, 0, len(logs))
|
|
for _, entry := range logs {
|
|
if entry.Data == nil {
|
|
lines = append(lines, "")
|
|
continue
|
|
}
|
|
lines = append(lines, decodeBase64(*entry.Data))
|
|
}
|
|
return mcp.NewToolResultText(render(map[string]any{
|
|
"stepId": stepID,
|
|
"logs": strings.Join(lines, "\n"),
|
|
})), nil
|
|
}
|
|
return tool, handler
|
|
}
|