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 }