Commit4cb884a7Recorded2 Apr 2026Repositoryfjo

Add Woodpecker CI pipeline reading tools

Message

Add MCP tools and CLI commands for listing and inspecting Woodpecker CI pipelines. Includes a woodpecker client library (separate from forgejo client due to different auth scheme and API path), pipeline API module, and fjo tool integration with repo lookup, step details, and formatting.

New env vars: WOODPECKERTOKEN (falls back to FORGEJOTOKEN), WOODPECKERURL (derived from FORGEJOURL by default).

Changed
 src/fjo/config.sgl           |  34 +++++++++++++++++++++++++++++++++
 src/fjo/main.sgl             |  29 +++++++++++++++++++++++++----
 src/fjo/pipelines.sgl        | 167 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/woodpecker/client.sgl    |  40 +++++++++++++++++++++++++++++++++++++++
 src/woodpecker/pipelines.sgl |  49 ++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 315 insertions(+), 4 deletions(-)
Diff
src/fjo/config.sglmodified
@@ -10,6 +10,8 @@
10
(sigil env))
11
(export fjo-token
12
fjo-base-url
+13
fjo-woodpecker-token
+14
fjo-woodpecker-url
15
fjo-load-env!)
16
(begin
17
@@ -35,4 +37,36 @@
37
(: -> string?)
38
(or (getenv "FORGEJO_URL") "https://codeberg.org"))
39
+40
;;; Get the Woodpecker CI API token.
+41
;;;
+42
;;; Uses WOODPECKER_TOKEN if set, otherwise falls back to FORGEJO_TOKEN.
+43
(define (fjo-woodpecker-token)
+44
(: -> string?)
+45
(let ((val (getenv "WOODPECKER_TOKEN")))
+46
(if (and val (not (string=? val "")))
+47
val
+48
(fjo-token))))
+49
+50
;;; Get the Woodpecker CI base URL.
+51
;;;
+52
;;; Uses WOODPECKER_URL if set, otherwise derives from FORGEJO_URL
+53
;;; by prepending "ci." to the hostname.
+54
(define (fjo-woodpecker-url)
+55
(: -> string?)
+56
(let ((val (getenv "WOODPECKER_URL")))
+57
(if (and val (not (string=? val "")))
+58
val
+59
(let ((forgejo-url (fjo-base-url)))
+60
(cond
+61
((string-contains? forgejo-url "codeberg.org")
+62
"https://ci.codeberg.org")
+63
(else
+64
(let ((proto-end (string-find forgejo-url "://")))
+65
(if proto-end
+66
(string-append (substring forgejo-url 0 (+ proto-end 3))
+67
"ci."
+68
(substring forgejo-url (+ proto-end 3)
+69
(string-length forgejo-url)))
+70
(string-append "https://ci." forgejo-url)))))))))
+71
72
))
src/fjo/main.sglmodified
@@ -13,7 +13,8 @@
13
(fjo repos)
14
(fjo issues)
15
(fjo pulls)
16
(fjo releases))
+16
(fjo releases)
+17
(fjo pipelines))
18
(export main)
19
(begin
20
@@ -27,6 +28,7 @@
28
(register-issue-tools! server mcp-server-register-tool!)
29
(register-pull-tools! server mcp-server-register-tool!)
30
(register-release-tools! server mcp-server-register-tool!)
+31
(register-pipeline-tools! server mcp-server-register-tool!)
32
(mcp-server-run server)))
33
34
;; ============================================================
@@ -49,10 +51,14 @@
51
(display " fjo issues create <owner> <repo> <t> Create issue\n")
52
(display " fjo prs list <owner> <repo> List PRs\n")
53
(display " fjo prs get <owner> <repo> <num> Get PR\n")
52
(display " fjo releases list <owner> <repo> List releases\n\n")
+54
(display " fjo releases list <owner> <repo> List releases\n")
+55
(display " fjo pipelines list <owner> <repo> List CI pipelines\n")
+56
(display " fjo pipelines get <owner> <repo> <n> Get pipeline details\n\n")
57
(display "Environment:\n")
54
(display " FORGEJO_TOKEN API token (required)\n")
55
(display " FORGEJO_URL Instance URL (default: https://codeberg.org)\n"))
+58
(display " FORGEJO_TOKEN API token (required)\n")
+59
(display " FORGEJO_URL Instance URL (default: https://codeberg.org)\n")
+60
(display " WOODPECKER_TOKEN Woodpecker CI token (default: FORGEJO_TOKEN)\n")
+61
(display " WOODPECKER_URL Woodpecker CI URL (default: derived from FORGEJO_URL)\n"))
62
63
(define (cli-dispatch args)
64
(if (< (length args) 2)
@@ -160,6 +166,21 @@
166
repo: (cadr rest) }))
167
(newline))
168
+169
;; pipelines
+170
((and (string=? group "pipelines") (string=? cmd "list")
+171
(>= (length rest) 2))
+172
(display (tool-pipelines-list #{ owner: (car rest)
+173
repo: (cadr rest) }))
+174
(newline))
+175
+176
((and (string=? group "pipelines") (string=? cmd "get")
+177
(>= (length rest) 3))
+178
(display (tool-pipelines-get
+179
#{ owner: (car rest)
+180
repo: (cadr rest)
+181
number: (string->number (caddr rest)) }))
+182
(newline))
+183
184
(else
185
(display (string-append "Unknown command: " group " " cmd "\n"))
186
(print-usage)
src/fjo/pipelines.sgladded
@@ -0,0 +1,167 @@
+1
;;; (fjo pipelines) - Woodpecker CI pipeline tools.
+2
;;;
+3
;;; MCP tools and CLI handlers for viewing Woodpecker CI pipeline
+4
;;; status with step details.
+5
+6
(define-library (fjo pipelines)
+7
(import (sigil core)
+8
(sigil string)
+9
(sigil json)
+10
(sigil array)
+11
(sigil mcp server)
+12
(woodpecker pipelines)
+13
(fjo config))
+14
(export register-pipeline-tools!
+15
tool-pipelines-list
+16
tool-pipelines-get)
+17
(begin
+18
+19
;; ============================================================
+20
;; Helpers
+21
;; ============================================================
+22
+23
;;; Look up a Woodpecker repo ID from owner/repo.
+24
(define (lookup-repo-id base-url token owner repo)
+25
(guard (e (else
+26
(error (string-append "Failed to look up Woodpecker repo "
+27
owner "/" repo ": "
+28
(exception-message e)))))
+29
(let ((result (woodpecker-repo-lookup base-url token
+30
(string-append owner "/" repo))))
+31
(dict-ref result id:))))
+32
+33
(define (truncate-message msg max-len)
+34
(let ((first-line (let ((nl (string-find msg "\n")))
+35
(if nl (substring msg 0 nl) msg))))
+36
(if (> (string-length first-line) max-len)
+37
(string-append (substring first-line 0 max-len) "...")
+38
first-line)))
+39
+40
;; ============================================================
+41
;; Tool Schemas
+42
;; ============================================================
+43
+44
(define pipelines-list-schema
+45
'((type . "object")
+46
(properties . ((owner . ((type . "string")
+47
(description . "Repository owner (user or org)")))
+48
(repo . ((type . "string")
+49
(description . "Repository name")))))
+50
(required . ("owner" "repo"))))
+51
+52
(define pipelines-get-schema
+53
'((type . "object")
+54
(properties . ((owner . ((type . "string")
+55
(description . "Repository owner (user or org)")))
+56
(repo . ((type . "string")
+57
(description . "Repository name")))
+58
(number . ((type . "integer")
+59
(description . "Pipeline number")))))
+60
(required . ("owner" "repo" "number"))))
+61
+62
;; ============================================================
+63
;; Formatting
+64
;; ============================================================
+65
+66
(define (format-pipeline p)
+67
(let ((number (dict-ref p number:))
+68
(status (dict-ref p status:))
+69
(branch (dict-ref p branch: ""))
+70
(event (dict-ref p event: ""))
+71
(message (dict-ref p message: ""))
+72
(author (dict-ref p author: "")))
+73
(string-append
+74
"#" (number->string number)
+75
" [" status "] "
+76
(if (not (string=? event ""))
+77
(string-append event " ")
+78
"")
+79
(if (not (string=? branch ""))
+80
(string-append "on " branch " ")
+81
"")
+82
(if (not (string=? author ""))
+83
(string-append "by " author " ")
+84
"")
+85
"- " (truncate-message message 60))))
+86
+87
(define (format-step s)
+88
(let ((name (dict-ref s name:))
+89
(state (dict-ref s state: "unknown"))
+90
(exit-code (dict-ref s exit_code: 0)))
+91
(string-append
+92
" " name " [" state "]"
+93
(if (not (= exit-code 0))
+94
(string-append " (exit " (number->string exit-code) ")")
+95
""))))
+96
+97
(define (format-pipeline-detail p steps)
+98
(let ((number (dict-ref p number:))
+99
(status (dict-ref p status:))
+100
(branch (dict-ref p branch: ""))
+101
(event (dict-ref p event: ""))
+102
(message (dict-ref p message: ""))
+103
(author (dict-ref p author: ""))
+104
(commit (dict-ref p commit: "")))
+105
(string-append
+106
"Pipeline #" (number->string number) "\n"
+107
"Status: " status "\n"
+108
"Event: " event "\n"
+109
"Branch: " branch "\n"
+110
"Commit: " (if (> (string-length commit) 8)
+111
(substring commit 0 8)
+112
commit) "\n"
+113
"Author: " author "\n"
+114
"Message: " (truncate-message message 80) "\n"
+115
"\nSteps:\n"
+116
(if (and steps (> (array-length steps) 0))
+117
(string-join (map format-step (array->list steps)) "\n")
+118
" (no steps)"))))
+119
+120
;; ============================================================
+121
;; Tool Handlers
+122
;; ============================================================
+123
+124
(define (tool-pipelines-list args)
+125
(let* ((owner (dict-ref args owner:))
+126
(repo (dict-ref args repo:))
+127
(base (fjo-woodpecker-url))
+128
(token (fjo-woodpecker-token))
+129
(repo-id (lookup-repo-id base token owner repo))
+130
(pipelines (woodpecker-list-pipelines base token repo-id)))
+131
(if (or (not pipelines) (= (array-length pipelines) 0))
+132
(string-append "No pipelines found for " owner "/" repo ".")
+133
(string-append
+134
"Pipelines for " owner "/" repo ":\n\n"
+135
(string-join (map format-pipeline (array->list pipelines)) "\n")))))
+136
+137
(define (tool-pipelines-get args)
+138
(let* ((owner (dict-ref args owner:))
+139
(repo (dict-ref args repo:))
+140
(number (dict-ref args number:))
+141
(base (fjo-woodpecker-url))
+142
(token (fjo-woodpecker-token))
+143
(repo-id (lookup-repo-id base token owner repo))
+144
(pipeline (woodpecker-get-pipeline base token repo-id number))
+145
(steps (woodpecker-list-steps base token repo-id number)))
+146
(if (not pipeline)
+147
(string-append "Pipeline #" (number->string number) " not found.")
+148
(format-pipeline-detail pipeline steps))))
+149
+150
;; ============================================================
+151
;; MCP Registration
+152
;; ============================================================
+153
+154
(define (register-pipeline-tools! server register)
+155
(register server
+156
"fjo/pipelines-list"
+157
"List recent Woodpecker CI pipelines for a repository."
+158
pipelines-list-schema
+159
tool-pipelines-list)
+160
+161
(register server
+162
"fjo/pipelines-get"
+163
"Get details of a specific pipeline with step status."
+164
pipelines-get-schema
+165
tool-pipelines-get))
+166
+167
))
src/woodpecker/client.sgladded
@@ -0,0 +1,40 @@
+1
;;; (woodpecker client) - Woodpecker CI API client utilities.
+2
;;;
+3
;;; Provides authentication headers and URL construction for the
+4
;;; Woodpecker CI API at ci.codeberg.org.
+5
+6
(define-library (woodpecker client)
+7
(import (sigil core)
+8
(sigil dict)
+9
(sigil string)
+10
(sigil json)
+11
(sigil http client))
+12
+13
(export woodpecker-auth-headers
+14
woodpecker-api-url
+15
woodpecker-get/json)
+16
+17
(begin
+18
+19
;;; Build authorization headers for the Woodpecker API.
+20
;;; Woodpecker uses Bearer token auth (unlike Forgejo's "token" prefix).
+21
(define (woodpecker-auth-headers token)
+22
#{ authorization: (string-append "Bearer " token) })
+23
+24
;;; Build an API URL from a base URL and path segments.
+25
;;;
+26
;;; (woodpecker-api-url "https://ci.codeberg.org" "repos" "123" "pipelines")
+27
;;; => "https://ci.codeberg.org/api/repos/123/pipelines"
+28
(define (woodpecker-api-url base-url . parts)
+29
(apply build-api-url base-url "api" parts))
+30
+31
;;; Response checker for the Woodpecker API.
+32
(define check-response
+33
(make-response-checker name: "Woodpecker API"))
+34
+35
;;; Authenticated JSON GET request.
+36
(define (woodpecker-get/json token url)
+37
(check-response
+38
(http-get url headers: (woodpecker-auth-headers token))))
+39
+40
))
src/woodpecker/pipelines.sgladded
@@ -0,0 +1,49 @@
+1
;;; (woodpecker pipelines) - Woodpecker CI pipeline API.
+2
;;;
+3
;;; List pipelines, get pipeline details with steps.
+4
+5
(define-library (woodpecker pipelines)
+6
(import (sigil core)
+7
(sigil string)
+8
(woodpecker client))
+9
+10
(export woodpecker-repo-lookup
+11
woodpecker-list-pipelines
+12
woodpecker-get-pipeline
+13
woodpecker-list-steps
+14
woodpecker-get-step-logs)
+15
+16
(begin
+17
+18
;;; Look up a Woodpecker repo by its full name (e.g. "owner/repo").
+19
;;; Returns the repo object including its numeric id.
+20
(define (woodpecker-repo-lookup base-url token full-name)
+21
(woodpecker-get/json token
+22
(woodpecker-api-url base-url "repos" "lookup" full-name)))
+23
+24
;;; List recent pipelines for a repo.
+25
;;; repo-id is the numeric Woodpecker repo ID.
+26
(define (woodpecker-list-pipelines base-url token repo-id)
+27
(woodpecker-get/json token
+28
(woodpecker-api-url base-url "repos" (number->string repo-id) "pipelines")))
+29
+30
;;; Get a specific pipeline by number.
+31
(define (woodpecker-get-pipeline base-url token repo-id number)
+32
(woodpecker-get/json token
+33
(woodpecker-api-url base-url "repos" (number->string repo-id)
+34
"pipelines" (number->string number))))
+35
+36
;;; List steps for a pipeline.
+37
(define (woodpecker-list-steps base-url token repo-id number)
+38
(woodpecker-get/json token
+39
(woodpecker-api-url base-url "repos" (number->string repo-id)
+40
"pipelines" (number->string number) "steps")))
+41
+42
;;; Get logs for a specific step.
+43
(define (woodpecker-get-step-logs base-url token repo-id pipeline-number step-id)
+44
(woodpecker-get/json token
+45
(woodpecker-api-url base-url "repos" (number->string repo-id)
+46
"logs" (number->string pipeline-number)
+47
(number->string step-id))))
+48
+49
))