feat: base MCP server with 25 workflow tools #2

Merged
forgejo_admin merged 3 commits from 1-base-mcp-server-with-25-workflow-tools into main 2026-03-01 21:39:09 +00:00
Contributor

Summary

  • Implement a full MCP server wrapping the Woodpecker CI SDK with 25 workflow-level tools across 6 modules (system, repos, pipelines, crons, secrets, queue)
  • All tools accept human-readable repo names and return curated JSON responses -- no raw SDK passthrough
  • Includes comprehensive integration test suite (19 tests) running against live Woodpecker

Changes

  • src/woodpecker_mcp/server.py: MCP server entry point with shared client lifecycle, _ok/_error_response helpers, lookup_repo name-to-ID resolver
  • src/woodpecker_mcp/tools/system.py: healthz, get_version (curated: version + source), get_current_user (curated: id, login, email, admin, active)
  • src/woodpecker_mcp/tools/repos.py: list_repos, get_repo with curated repo metadata
  • src/woodpecker_mcp/tools/pipelines.py: trigger_pipeline, get_pipeline_status, get_pipeline_logs (smart step discovery, max_lines truncation, exception messages in unavailable logs), restart_pipeline, cancel_pipeline, list_pipelines with filtering
  • src/woodpecker_mcp/tools/crons.py: Full CRUD + run for cron jobs with page/per_page pagination on list, clean cron_id_str conversion
  • src/woodpecker_mcp/tools/secrets.py: Repo + global secret CRUD (8 tools) with page/per_page pagination on list endpoints
  • src/woodpecker_mcp/tools/queue.py: get_queue_status with curated queue info (stats, paused flag) + queued pipeline summaries
  • tests/: 19 integration tests including pipeline mutation tests (trigger, cancel, restart) with proper pytest.skip error handling

Test Plan

  • Tests pass locally (17 passed, 2 skipped due to no pipeline config on test repo)
  • Ruff linting passes with zero issues
  • Manual verification of curated response shapes for get_version and get_queue_status
  • No regressions in existing tools after QA fix round

Review Checklist

  • Passed automated review-fix loop (6 QA nits addressed)
  • No secrets committed
  • No unnecessary file changes
  • Commit messages are descriptive
  • issue-woodpecker-mcp-base-server -- the issue this PR addresses
  • plan-2026-02-28-woodpecker-mcp -- Phase 1a: Base MCP server with 25 workflow tools
## Summary - Implement a full MCP server wrapping the Woodpecker CI SDK with 25 workflow-level tools across 6 modules (system, repos, pipelines, crons, secrets, queue) - All tools accept human-readable repo names and return curated JSON responses -- no raw SDK passthrough - Includes comprehensive integration test suite (19 tests) running against live Woodpecker ## Changes - `src/woodpecker_mcp/server.py`: MCP server entry point with shared client lifecycle, `_ok`/`_error_response` helpers, `lookup_repo` name-to-ID resolver - `src/woodpecker_mcp/tools/system.py`: `healthz`, `get_version` (curated: version + source), `get_current_user` (curated: id, login, email, admin, active) - `src/woodpecker_mcp/tools/repos.py`: `list_repos`, `get_repo` with curated repo metadata - `src/woodpecker_mcp/tools/pipelines.py`: `trigger_pipeline`, `get_pipeline_status`, `get_pipeline_logs` (smart step discovery, `max_lines` truncation, exception messages in unavailable logs), `restart_pipeline`, `cancel_pipeline`, `list_pipelines` with filtering - `src/woodpecker_mcp/tools/crons.py`: Full CRUD + run for cron jobs with `page`/`per_page` pagination on list, clean `cron_id_str` conversion - `src/woodpecker_mcp/tools/secrets.py`: Repo + global secret CRUD (8 tools) with `page`/`per_page` pagination on list endpoints - `src/woodpecker_mcp/tools/queue.py`: `get_queue_status` with curated queue info (stats, paused flag) + queued pipeline summaries - `tests/`: 19 integration tests including pipeline mutation tests (`trigger`, `cancel`, `restart`) with proper `pytest.skip` error handling ## Test Plan - [x] Tests pass locally (17 passed, 2 skipped due to no pipeline config on test repo) - [x] Ruff linting passes with zero issues - [x] Manual verification of curated response shapes for get_version and get_queue_status - [x] No regressions in existing tools after QA fix round ## Review Checklist - [x] Passed automated review-fix loop (6 QA nits addressed) - [x] No secrets committed - [x] No unnecessary file changes - [x] Commit messages are descriptive ## Related Notes - `issue-woodpecker-mcp-base-server` -- the issue this PR addresses - `plan-2026-02-28-woodpecker-mcp` -- Phase 1a: Base MCP server with 25 workflow tools
FastMCP server wrapping the Woodpecker CI SDK into 25 focused MCP tools
across 6 domain files: pipelines (6), repos (2), secrets (8), crons (5),
system (3), queue (1). All tools use get_client() from server.py for
mcp-remote compatibility (Phase 2). Smart log fetching auto-discovers
pipeline steps and returns failed step logs by default.

Integration tests pass against live Woodpecker instance (16 tests).

Closes #1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Contributor

Self-Review: LGTM

Review completed. All checks pass:

Architecture

  • All 6 tool modules import get_client from server.py -- _client global is never accessed directly from tools (verified via grep: 0 direct references)
  • lookup_repo() shared helper used by all repo-scoped tools
  • register_all_tools() correctly imports all 6 modules

Tool Count: 25/25

  • pipelines.py: 6 @mcp.tool() decorators
  • repos.py: 2
  • secrets.py: 8
  • crons.py: 5
  • system.py: 3
  • queue.py: 1

Error Handling

  • All 25 tools have try/except with return _error_response(exc) (verified: 25 occurrences across 6 files)

Tests: 16/16 passed

  • System: healthz, get_version, get_current_user
  • Repos: list_repos, get_repo
  • Pipelines: list, status, logs (smart fetching), bad step name error
  • Secrets: repo CRUD, global CRUD
  • Crons: list, CRUD cycle
  • Queue: get_queue_status

Lint: Clean

  • ruff check src/ tests/ -- 0 errors
## Self-Review: LGTM **Review completed.** All checks pass: ### Architecture - All 6 tool modules import `get_client` from `server.py` -- `_client` global is never accessed directly from tools (verified via grep: 0 direct references) - `lookup_repo()` shared helper used by all repo-scoped tools - `register_all_tools()` correctly imports all 6 modules ### Tool Count: 25/25 - pipelines.py: 6 `@mcp.tool()` decorators - repos.py: 2 - secrets.py: 8 - crons.py: 5 - system.py: 3 - queue.py: 1 ### Error Handling - All 25 tools have `try/except` with `return _error_response(exc)` (verified: 25 occurrences across 6 files) ### Tests: 16/16 passed - System: healthz, get_version, get_current_user - Repos: list_repos, get_repo - Pipelines: list, status, logs (smart fetching), bad step name error - Secrets: repo CRUD, global CRUD - Crons: list, CRUD cycle - Queue: get_queue_status ### Lint: Clean - `ruff check src/ tests/` -- 0 errors
Author
Contributor

PR #2 Review

Reviewer: QA Agent (fresh context)
Scope: Phase 1a -- base MCP server with 25 workflow tools
Reference plan: plan-2026-02-28-woodpecker-mcp


BLOCKERS

None found. The implementation is solid.


NITS

1. list_pipelines does not expose before / after / ref SDK parameters
The SDK's list_pipelines method accepts before, after, and ref parameters in addition to branch, status, event, page, and per_page. The MCP tool only exposes the latter five. This is arguably correct curation (agents rarely need date-range filtering), but worth noting for completeness. Non-blocking.

2. get_version returns raw API response without curation
In /home/ldraney/woodpecker-mcp/src/woodpecker_mcp/tools/system.py line 29, get_version() passes the SDK result directly through _ok(result) without curating fields. Every other tool curates its response. The version endpoint's response is small enough that this is fine in practice, but it breaks the pattern. Non-blocking.

3. test_list_pipelines silently swallows errors
In /home/ldraney/woodpecker-mcp/tests/test_pipelines.py lines 13-16, test_list_pipelines catches error responses with a bare return instead of skipping with pytest.skip(). This means if the API returns an unexpected error, the test passes silently rather than signaling that something went wrong. Non-blocking but worth tightening.

4. No tests for trigger_pipeline, restart_pipeline, cancel_pipeline
These three mutating pipeline tools have no integration tests. The 16 tests cover read operations and CRUD for secrets/crons, but the three pipeline-mutating operations are untested. Understandable for integration tests (triggering/cancelling pipelines has side effects), but worth noting. Non-blocking.

5. update_cron_job and cron delete/run accept cron_id: int but the SDK expects str
In /home/ldraney/woodpecker-mcp/src/woodpecker_mcp/tools/crons.py lines 82, 96, 110, the MCP tool accepts cron_id: Annotated[int, ...] but then calls str(cron_id) to pass it to the SDK. This works, but the type annotation misleads: agents will see int in the MCP schema, then the code silently converts. Consider making the annotation consistent (either accept str from the agent or document why int is the interface). Non-blocking.

6. queue_info in get_queue_status is passed through raw
In /home/ldraney/woodpecker-mcp/src/woodpecker_mcp/tools/queue.py line 17, queue_info = client.get_queue_info() is returned without curation (line 35: "queue_info": queue_info). The queued pipelines are curated, but the queue_info dict is raw. Minor inconsistency. Non-blocking.


ARCHITECTURE COMPLIANCE

All items from the plan's architecture checklist verified:

  • Module-level mcp = FastMCP("woodpecker") singleton -- Yes, in /home/ldraney/woodpecker-mcp/src/woodpecker_mcp/server.py line 12.
  • Lazy client via get_client() -- Yes, in server.py lines 17-27. Reads WOODPECKER_URL and WOODPECKER_TOKEN from environment via the SDK's constructor.
  • __init__.py exports mcp via __all__ -- Yes, in /home/ldraney/woodpecker-mcp/src/woodpecker_mcp/__init__.py line 5.
  • register_all_tools() in tools/__init__.py -- Yes, imports all 6 domain modules.
  • All tools return str -- Verified. Every tool function has -> str return type.
  • All tools have try/except -- Verified. Every tool wraps its body in try/except Exception and returns _error_response(exc).
  • Parameters use Annotated[type, Field(description="...")] -- Verified across all tool modules.
  • Response shaping -- All tools curate responses (except get_version and queue_info -- see nits).
  • get_pipeline_logs does smart step discovery -- Yes, auto-discovers steps from the pipeline, returns failed step by default, supports specific step name, and handles "step not found" gracefully.

CRITICAL: get_client() PATCHABILITY

PASS. Verified every tool file:

File Imports get_client from ..server Direct _client usage
tools/pipelines.py Yes (line 10) None
tools/repos.py Yes (line 9) None
tools/secrets.py Yes (line 9) None
tools/crons.py Yes (line 9) None
tools/system.py Yes (line 5) None
tools/queue.py Yes (line 5) None

The lookup_repo() helper in server.py also calls get_client() (line 66), not _client directly. Phase 2 mcp-remote patching will work.


25 TOOLS INVENTORY

Domain File Tools Count
Pipeline pipelines.py trigger_pipeline, get_pipeline_status, get_pipeline_logs, restart_pipeline, cancel_pipeline, list_pipelines 6
Repo repos.py list_repos, get_repo 2
Repo Secrets secrets.py list_repo_secrets, create_repo_secret, update_repo_secret, delete_repo_secret 4
Global Secrets secrets.py list_global_secrets, create_global_secret, update_global_secret, delete_global_secret 4
Crons crons.py list_cron_jobs, create_cron_job, update_cron_job, delete_cron_job, run_cron_job 5
System system.py healthz, get_version, get_current_user 3
Queue queue.py get_queue_status 1
Total 25

All 25 tools from the plan are implemented.


TEST COVERAGE

16 integration tests across 6 test files:

File Tests Coverage
test_system.py 3 healthz, get_version, get_current_user
test_repos.py 2 list_repos, get_repo
test_pipelines.py 4 list, status, logs, logs-bad-step
test_crons.py 2 list, CRUD cycle
test_secrets.py 4 repo list, repo CRUD, global list, global CRUD
test_queue.py 1 get_queue_status
Total 16

Untested tools (by design -- mutating operations with side effects): trigger_pipeline, restart_pipeline, cancel_pipeline.


SOP COMPLIANCE

  • Branch named after issue -- Branch is 1-base-mcp-server-with-25-workflow-tools, issue is #1. Correct.
  • PR body follows template-pr-body -- Could not extract PR body from the diff output (single-line JSON exceeded processing limits). Unable to verify sections: Summary, Changes, Test Plan, Review Checklist, Related Notes. Flagged for manual verification.
  • Related Notes references plan slug -- Unable to verify (depends on PR body). The plan slug should be plan-2026-02-28-woodpecker-mcp.
  • Tests exist and pass -- 16 integration tests exist. Tests call tool functions directly against live Woodpecker API.
  • No secrets, .env files, or credentials committed -- .env is in .gitignore. Credentials loaded from ~/secrets/woodpecker/credentials.env at test time only.
  • No unnecessary file changes (scope creep) -- All files are within expected scope: server.py, 6 tool modules, 6 test files, conftest, pyproject.toml, README, .gitignore.
  • Commit messages are descriptive -- PR title is "feat: base MCP server with 25 workflow tools". Follows conventional commit format.
  • No hardcoded secrets -- Verified via grep. No tokens, passwords, or API keys in source.

VERDICT: APPROVED

The implementation is clean, well-structured, and faithfully follows the forgejo-mcp pattern. All 25 tools are present, properly annotated, and use get_client() for Phase 2 patchability. The code is well-organized across 6 domain files. Error handling is consistent. The 16 integration tests provide reasonable coverage.

The nits are all non-blocking quality suggestions. The two SOP compliance items I could not verify (PR body template and Related Notes) should be checked manually by the maintainer before merge.

## PR #2 Review **Reviewer:** QA Agent (fresh context) **Scope:** Phase 1a -- base MCP server with 25 workflow tools **Reference plan:** `plan-2026-02-28-woodpecker-mcp` --- ### BLOCKERS None found. The implementation is solid. --- ### NITS **1. `list_pipelines` does not expose `before` / `after` / `ref` SDK parameters** The SDK's `list_pipelines` method accepts `before`, `after`, and `ref` parameters in addition to `branch`, `status`, `event`, `page`, and `per_page`. The MCP tool only exposes the latter five. This is arguably correct curation (agents rarely need date-range filtering), but worth noting for completeness. Non-blocking. **2. `get_version` returns raw API response without curation** In `/home/ldraney/woodpecker-mcp/src/woodpecker_mcp/tools/system.py` line 29, `get_version()` passes the SDK result directly through `_ok(result)` without curating fields. Every other tool curates its response. The version endpoint's response is small enough that this is fine in practice, but it breaks the pattern. Non-blocking. **3. `test_list_pipelines` silently swallows errors** In `/home/ldraney/woodpecker-mcp/tests/test_pipelines.py` lines 13-16, `test_list_pipelines` catches error responses with a bare `return` instead of skipping with `pytest.skip()`. This means if the API returns an unexpected error, the test passes silently rather than signaling that something went wrong. Non-blocking but worth tightening. **4. No tests for `trigger_pipeline`, `restart_pipeline`, `cancel_pipeline`** These three mutating pipeline tools have no integration tests. The 16 tests cover read operations and CRUD for secrets/crons, but the three pipeline-mutating operations are untested. Understandable for integration tests (triggering/cancelling pipelines has side effects), but worth noting. Non-blocking. **5. `update_cron_job` and cron delete/run accept `cron_id: int` but the SDK expects `str`** In `/home/ldraney/woodpecker-mcp/src/woodpecker_mcp/tools/crons.py` lines 82, 96, 110, the MCP tool accepts `cron_id: Annotated[int, ...]` but then calls `str(cron_id)` to pass it to the SDK. This works, but the type annotation misleads: agents will see `int` in the MCP schema, then the code silently converts. Consider making the annotation consistent (either accept `str` from the agent or document why `int` is the interface). Non-blocking. **6. `queue_info` in `get_queue_status` is passed through raw** In `/home/ldraney/woodpecker-mcp/src/woodpecker_mcp/tools/queue.py` line 17, `queue_info = client.get_queue_info()` is returned without curation (line 35: `"queue_info": queue_info`). The queued pipelines are curated, but the queue_info dict is raw. Minor inconsistency. Non-blocking. --- ### ARCHITECTURE COMPLIANCE All items from the plan's architecture checklist verified: - **Module-level `mcp = FastMCP("woodpecker")` singleton** -- Yes, in `/home/ldraney/woodpecker-mcp/src/woodpecker_mcp/server.py` line 12. - **Lazy client via `get_client()`** -- Yes, in server.py lines 17-27. Reads WOODPECKER_URL and WOODPECKER_TOKEN from environment via the SDK's constructor. - **`__init__.py` exports `mcp` via `__all__`** -- Yes, in `/home/ldraney/woodpecker-mcp/src/woodpecker_mcp/__init__.py` line 5. - **`register_all_tools()` in `tools/__init__.py`** -- Yes, imports all 6 domain modules. - **All tools return `str`** -- Verified. Every tool function has `-> str` return type. - **All tools have try/except** -- Verified. Every tool wraps its body in `try/except Exception` and returns `_error_response(exc)`. - **Parameters use `Annotated[type, Field(description="...")]`** -- Verified across all tool modules. - **Response shaping** -- All tools curate responses (except `get_version` and `queue_info` -- see nits). - **`get_pipeline_logs` does smart step discovery** -- Yes, auto-discovers steps from the pipeline, returns failed step by default, supports specific step name, and handles "step not found" gracefully. --- ### CRITICAL: get_client() PATCHABILITY **PASS.** Verified every tool file: | File | Imports `get_client` from `..server` | Direct `_client` usage | |------|--------------------------------------|----------------------| | `tools/pipelines.py` | Yes (line 10) | None | | `tools/repos.py` | Yes (line 9) | None | | `tools/secrets.py` | Yes (line 9) | None | | `tools/crons.py` | Yes (line 9) | None | | `tools/system.py` | Yes (line 5) | None | | `tools/queue.py` | Yes (line 5) | None | The `lookup_repo()` helper in server.py also calls `get_client()` (line 66), not `_client` directly. Phase 2 mcp-remote patching will work. --- ### 25 TOOLS INVENTORY | Domain | File | Tools | Count | |--------|------|-------|-------| | Pipeline | `pipelines.py` | trigger_pipeline, get_pipeline_status, get_pipeline_logs, restart_pipeline, cancel_pipeline, list_pipelines | 6 | | Repo | `repos.py` | list_repos, get_repo | 2 | | Repo Secrets | `secrets.py` | list_repo_secrets, create_repo_secret, update_repo_secret, delete_repo_secret | 4 | | Global Secrets | `secrets.py` | list_global_secrets, create_global_secret, update_global_secret, delete_global_secret | 4 | | Crons | `crons.py` | list_cron_jobs, create_cron_job, update_cron_job, delete_cron_job, run_cron_job | 5 | | System | `system.py` | healthz, get_version, get_current_user | 3 | | Queue | `queue.py` | get_queue_status | 1 | | **Total** | | | **25** | All 25 tools from the plan are implemented. --- ### TEST COVERAGE 16 integration tests across 6 test files: | File | Tests | Coverage | |------|-------|---------| | `test_system.py` | 3 | healthz, get_version, get_current_user | | `test_repos.py` | 2 | list_repos, get_repo | | `test_pipelines.py` | 4 | list, status, logs, logs-bad-step | | `test_crons.py` | 2 | list, CRUD cycle | | `test_secrets.py` | 4 | repo list, repo CRUD, global list, global CRUD | | `test_queue.py` | 1 | get_queue_status | | **Total** | **16** | | Untested tools (by design -- mutating operations with side effects): `trigger_pipeline`, `restart_pipeline`, `cancel_pipeline`. --- ### SOP COMPLIANCE - [x] **Branch named after issue** -- Branch is `1-base-mcp-server-with-25-workflow-tools`, issue is #1. Correct. - [ ] **PR body follows `template-pr-body`** -- Could not extract PR body from the diff output (single-line JSON exceeded processing limits). Unable to verify sections: Summary, Changes, Test Plan, Review Checklist, Related Notes. **Flagged for manual verification.** - [ ] **Related Notes references plan slug** -- Unable to verify (depends on PR body). The plan slug should be `plan-2026-02-28-woodpecker-mcp`. - [x] **Tests exist and pass** -- 16 integration tests exist. Tests call tool functions directly against live Woodpecker API. - [x] **No secrets, .env files, or credentials committed** -- `.env` is in `.gitignore`. Credentials loaded from `~/secrets/woodpecker/credentials.env` at test time only. - [x] **No unnecessary file changes (scope creep)** -- All files are within expected scope: server.py, 6 tool modules, 6 test files, conftest, pyproject.toml, README, .gitignore. - [x] **Commit messages are descriptive** -- PR title is "feat: base MCP server with 25 workflow tools". Follows conventional commit format. - [x] **No hardcoded secrets** -- Verified via grep. No tokens, passwords, or API keys in source. --- ### VERDICT: APPROVED The implementation is clean, well-structured, and faithfully follows the forgejo-mcp pattern. All 25 tools are present, properly annotated, and use `get_client()` for Phase 2 patchability. The code is well-organized across 6 domain files. Error handling is consistent. The 16 integration tests provide reasonable coverage. The nits are all non-blocking quality suggestions. The two SOP compliance items I could not verify (PR body template and Related Notes) should be checked manually by the maintainer before merge.
1. Curate raw responses: get_version returns only version/source fields,
   get_queue_status curates queue_info to stats and paused flag
2. Clean cron_id int-to-str: convert once via local variable at top of
   each function instead of inline str() at call sites
3. Fix silent error swallowing: test_list_pipelines uses pytest.skip
   instead of bare return; get_pipeline_logs includes exception in message
4. Add integration tests for trigger_pipeline, cancel_pipeline,
   restart_pipeline with proper pytest.skip on API errors
5. Expose SDK pagination params: page/per_page on list_cron_jobs,
   list_repo_secrets, list_global_secrets; add max_lines param to
   get_pipeline_logs for log truncation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Contributor

QA Fix Round

Addressed all 6 non-blocking nits from QA review:

  1. Curated raw responses from get_version (version + source only) and get_queue_status (stats + paused flag, no raw task arrays)
  2. Cleaned up cron_id int-to-str conversion -- convert once via cron_id_str local variable at top of each function
  3. Fixed test error handling -- test_list_pipelines uses pytest.skip instead of silent return; get_pipeline_logs includes exception message in unavailable log text
  4. Added tests for 3 mutating pipeline tools -- test_trigger_pipeline, test_cancel_pipeline, test_restart_pipeline with proper pytest.skip on API errors
  5. Exposed missing SDK filter params -- page/per_page on list_cron_jobs, list_repo_secrets, list_global_secrets; max_lines on get_pipeline_logs for log truncation
  6. Updated PR body to match template-pr-body format

All 17 tests pass, 2 skipped (trigger/cancel require pipeline config on test repo). Ruff clean.

Ready for fresh QA review.

## QA Fix Round Addressed all 6 non-blocking nits from QA review: 1. **Curated raw responses** from `get_version` (version + source only) and `get_queue_status` (stats + paused flag, no raw task arrays) 2. **Cleaned up cron_id int-to-str conversion** -- convert once via `cron_id_str` local variable at top of each function 3. **Fixed test error handling** -- `test_list_pipelines` uses `pytest.skip` instead of silent `return`; `get_pipeline_logs` includes exception message in unavailable log text 4. **Added tests for 3 mutating pipeline tools** -- `test_trigger_pipeline`, `test_cancel_pipeline`, `test_restart_pipeline` with proper `pytest.skip` on API errors 5. **Exposed missing SDK filter params** -- `page`/`per_page` on `list_cron_jobs`, `list_repo_secrets`, `list_global_secrets`; `max_lines` on `get_pipeline_logs` for log truncation 6. **Updated PR body** to match `template-pr-body` format All 17 tests pass, 2 skipped (trigger/cancel require pipeline config on test repo). Ruff clean. Ready for fresh QA review.
Author
Contributor

PR #2 Review (Fresh QA -- Round 2)

BLOCKERS

None.

Previous QA Nits (6 items) -- Status

1. get_version and get_queue_info should return curated responses (not raw): FIXED

  • get_version() in system.py now returns curated {"version": ..., "source": ...} instead of raw SDK passthrough.
  • get_queue_status() in queue.py now curates queue_info to {paused, worker_count, running_count, pending_count, waiting_on_deps_count} and shapes queued pipelines into summaries.

2. cron_id int-to-str should be cleaner (single conversion, not scattered): FIXED

  • update_cron_job, delete_cron_job, and run_cron_job in crons.py all accept cron_id: Annotated[int, ...] from MCP and convert once with cron_id_str = str(cron_id) before passing to the SDK. Clean and consistent.

3. Test error handling should use pytest.skip(), not silent return: FIXED

  • All tests that might hit API errors now use pytest.skip(f"API error: {result}") pattern. Verified in test_pipelines.py (lines 23, 63, 74, 79, 87), conftest.py (lines 31, 50, 52, 65).

4. Tests should exist for trigger_pipeline, restart_pipeline, cancel_pipeline: FIXED

  • test_trigger_pipeline exists (test_pipelines.py:59)
  • test_cancel_pipeline exists (test_pipelines.py:69) -- triggers a pipeline first then cancels it
  • test_restart_pipeline exists (test_pipelines.py:83) -- restarts the first pipeline

5. SDK filter params should be exposed (page/per_page on list tools, max_lines on logs): FIXED

  • list_pipelines exposes branch, status, event, page, per_page (matches SDK surface)
  • list_repo_secrets and list_global_secrets expose page, per_page
  • list_cron_jobs exposes page, per_page
  • get_pipeline_logs exposes max_lines for log truncation

6. PR body should match template: FIXED

  • PR body contains all 5 required sections: Summary, Changes, Test Plan, Review Checklist, Related Notes.
  • Related Notes references issue-woodpecker-mcp-base-server and Phase 1a.

NITS

Nit 1 (non-blocking): list_repos does not expose page/per_page or active filter
The SDK's list_repos() method supports active: bool, page: int, and per_page: int parameters, but the MCP tool takes no arguments at all. For consistency with other list tools that do expose pagination, consider adding these. Low priority since repos lists are typically small.

Nit 2 (non-blocking): Related Notes section does not reference the plan slug
The PR body's Related Notes references issue-woodpecker-mcp-base-server but does not reference the plan slug plan-2026-02-28-woodpecker-mcp. Per template-pr-body, the Related Notes section should include plan-slug -- the plan phase this implements.

Nit 3 (non-blocking): list_pipelines does not expose before/after/ref SDK parameters
The SDK's list_pipelines supports before, after, and ref filters. These are omitted from the MCP tool. Reasonable to omit for workflow-level use, but noting for completeness.

SOP COMPLIANCE

  • Branch named after issue (1-base-mcp-server-with-25-workflow-tools -- references issue #1)
  • PR body follows template-pr-body (all 5 sections present: Summary, Changes, Test Plan, Review Checklist, Related Notes)
  • [~] Related Notes references issue slug but missing plan slug plan-2026-02-28-woodpecker-mcp (non-blocking)
  • Tests exist and cover all 25 tools (19 integration tests, including CRUD cycles)
  • No secrets, .env files, or credentials committed (.env in .gitignore, creds loaded from ~/secrets/ at runtime)
  • No unnecessary file changes (scope matches Phase 1a exactly)
  • Commit messages are descriptive
  • All 25 tools present across 6 modules (matches plan exactly)
  • All tools import get_client from server.py (patchable for mcp-remote)
  • All tools use _ok()/_error_response() helpers consistently
  • All tools return curated responses, no raw SDK passthrough
  • __init__.py exports mcp via __all__
  • register_all_tools() imports all 6 tool modules
  • pyproject.toml dependencies correct (mcp>=1.0, ldraney-woodpecker-sdk>=0.1.0)
  • Console script entry point configured (woodpecker-mcp = woodpecker_mcp.server:main)

Architecture Quality Notes

  • get_client() patchability: All 6 tool modules import get_client from ..server. None access _client directly. Phase 2 remote wrapper will work cleanly.
  • Response curation: Every tool selects specific fields from SDK responses. No tool returns raw API dumps.
  • Error handling: Every tool wraps its body in try/except Exception with _error_response(exc). The httpx-specific branch in _error_response extracts status codes and response bodies.
  • Test fixtures: conftest.py properly loads credentials from ~/secrets/woodpecker/credentials.env, skips if unavailable, and provides session-scoped fixtures.
  • Smart log fetching: get_pipeline_logs auto-discovers steps, defaults to failed step logs, supports step name filtering and line truncation. Well designed.

VERDICT: APPROVED

All 6 previous nits are resolved. The 3 new nits are non-blocking style items. The implementation is clean, consistent, and follows the established MCP patterns exactly. All 25 tools are present, properly curated, and tested. Ready for merge.

## PR #2 Review (Fresh QA -- Round 2) ### BLOCKERS None. ### Previous QA Nits (6 items) -- Status **1. get_version and get_queue_info should return curated responses (not raw): FIXED** - `get_version()` in `system.py` now returns curated `{"version": ..., "source": ...}` instead of raw SDK passthrough. - `get_queue_status()` in `queue.py` now curates `queue_info` to `{paused, worker_count, running_count, pending_count, waiting_on_deps_count}` and shapes queued pipelines into summaries. **2. cron_id int-to-str should be cleaner (single conversion, not scattered): FIXED** - `update_cron_job`, `delete_cron_job`, and `run_cron_job` in `crons.py` all accept `cron_id: Annotated[int, ...]` from MCP and convert once with `cron_id_str = str(cron_id)` before passing to the SDK. Clean and consistent. **3. Test error handling should use pytest.skip(), not silent return: FIXED** - All tests that might hit API errors now use `pytest.skip(f"API error: {result}")` pattern. Verified in `test_pipelines.py` (lines 23, 63, 74, 79, 87), `conftest.py` (lines 31, 50, 52, 65). **4. Tests should exist for trigger_pipeline, restart_pipeline, cancel_pipeline: FIXED** - `test_trigger_pipeline` exists (test_pipelines.py:59) - `test_cancel_pipeline` exists (test_pipelines.py:69) -- triggers a pipeline first then cancels it - `test_restart_pipeline` exists (test_pipelines.py:83) -- restarts the first pipeline **5. SDK filter params should be exposed (page/per_page on list tools, max_lines on logs): FIXED** - `list_pipelines` exposes `branch`, `status`, `event`, `page`, `per_page` (matches SDK surface) - `list_repo_secrets` and `list_global_secrets` expose `page`, `per_page` - `list_cron_jobs` exposes `page`, `per_page` - `get_pipeline_logs` exposes `max_lines` for log truncation **6. PR body should match template: FIXED** - PR body contains all 5 required sections: Summary, Changes, Test Plan, Review Checklist, Related Notes. - Related Notes references `issue-woodpecker-mcp-base-server` and Phase 1a. ### NITS **Nit 1 (non-blocking): `list_repos` does not expose `page`/`per_page` or `active` filter** The SDK's `list_repos()` method supports `active: bool`, `page: int`, and `per_page: int` parameters, but the MCP tool takes no arguments at all. For consistency with other list tools that do expose pagination, consider adding these. Low priority since repos lists are typically small. **Nit 2 (non-blocking): Related Notes section does not reference the plan slug** The PR body's Related Notes references `issue-woodpecker-mcp-base-server` but does not reference the plan slug `plan-2026-02-28-woodpecker-mcp`. Per `template-pr-body`, the Related Notes section should include `plan-slug -- the plan phase this implements`. **Nit 3 (non-blocking): `list_pipelines` does not expose `before`/`after`/`ref` SDK parameters** The SDK's `list_pipelines` supports `before`, `after`, and `ref` filters. These are omitted from the MCP tool. Reasonable to omit for workflow-level use, but noting for completeness. ### SOP COMPLIANCE - [x] Branch named after issue (`1-base-mcp-server-with-25-workflow-tools` -- references issue #1) - [x] PR body follows `template-pr-body` (all 5 sections present: Summary, Changes, Test Plan, Review Checklist, Related Notes) - [~] Related Notes references issue slug but **missing plan slug** `plan-2026-02-28-woodpecker-mcp` (non-blocking) - [x] Tests exist and cover all 25 tools (19 integration tests, including CRUD cycles) - [x] No secrets, .env files, or credentials committed (`.env` in `.gitignore`, creds loaded from `~/secrets/` at runtime) - [x] No unnecessary file changes (scope matches Phase 1a exactly) - [x] Commit messages are descriptive - [x] All 25 tools present across 6 modules (matches plan exactly) - [x] All tools import `get_client` from `server.py` (patchable for mcp-remote) - [x] All tools use `_ok()`/`_error_response()` helpers consistently - [x] All tools return curated responses, no raw SDK passthrough - [x] `__init__.py` exports `mcp` via `__all__` - [x] `register_all_tools()` imports all 6 tool modules - [x] `pyproject.toml` dependencies correct (`mcp>=1.0`, `ldraney-woodpecker-sdk>=0.1.0`) - [x] Console script entry point configured (`woodpecker-mcp = woodpecker_mcp.server:main`) ### Architecture Quality Notes - **get_client() patchability**: All 6 tool modules import `get_client` from `..server`. None access `_client` directly. Phase 2 remote wrapper will work cleanly. - **Response curation**: Every tool selects specific fields from SDK responses. No tool returns raw API dumps. - **Error handling**: Every tool wraps its body in `try/except Exception` with `_error_response(exc)`. The httpx-specific branch in `_error_response` extracts status codes and response bodies. - **Test fixtures**: `conftest.py` properly loads credentials from `~/secrets/woodpecker/credentials.env`, skips if unavailable, and provides session-scoped fixtures. - **Smart log fetching**: `get_pipeline_logs` auto-discovers steps, defaults to failed step logs, supports step name filtering and line truncation. Well designed. ### VERDICT: APPROVED All 6 previous nits are resolved. The 3 new nits are non-blocking style items. The implementation is clean, consistent, and follows the established MCP patterns exactly. All 25 tools are present, properly curated, and tested. Ready for merge.
- list_repos: add active, page, per_page optional params matching SDK
- list_pipelines: add before, after, ref optional params matching SDK
- All 17 integration tests pass, ruff lint clean

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Contributor

QA Fix Round 2

Addressed 2 non-blocking nits from fresh QA review (3rd was already fixed):

  1. list_repos params: Added active, page, per_page optional params matching SDK signature
  2. PR body Related Notes: Already contained both issue-woodpecker-mcp-base-server and plan-2026-02-28-woodpecker-mcp -- no change needed
  3. list_pipelines params: Added before, after, ref optional params matching SDK signature

All 17 integration tests pass. Ruff lint clean.

Ready for fresh QA review.

## QA Fix Round 2 Addressed 2 non-blocking nits from fresh QA review (3rd was already fixed): 1. **list_repos params**: Added `active`, `page`, `per_page` optional params matching SDK signature 2. **PR body Related Notes**: Already contained both `issue-woodpecker-mcp-base-server` and `plan-2026-02-28-woodpecker-mcp` -- no change needed 3. **list_pipelines params**: Added `before`, `after`, `ref` optional params matching SDK signature All 17 integration tests pass. Ruff lint clean. Ready for fresh QA review.
Author
Contributor

PR #2 Review (Round 3 -- Fresh QA)

BLOCKERS

None.

NITS

None.

PREVIOUS NIT VERIFICATION (9/9 FIXED)

Round 1 nits (6/6):

# Nit Status Evidence
1 get_version and get_queue_info should return curated responses FIXED get_version() returns only version/source (system.py:29-33). get_queue_status() curates into paused, worker_count, running_count, pending_count, waiting_on_deps_count (queue.py:35-42).
2 cron_id int-to-str should be clean (single conversion) FIXED cron_id_str = str(cron_id) is a single conversion at point of use in all three cron tools (crons.py:77, 99, 114).
3 Test error handling should use pytest.skip(), not silent return FIXED test_trigger_pipeline, test_cancel_pipeline, test_restart_pipeline all use pytest.skip() for API errors (test_pipelines.py:62-63, 73-74, 86-87).
4 Tests should exist for trigger_pipeline, restart_pipeline, cancel_pipeline FIXED All three have dedicated tests (test_pipelines.py:59-89). test_cancel_pipeline even triggers a pipeline first to have something to cancel.
5 SDK filter params exposed (pagination on list tools, max_lines on logs) FIXED All list tools expose page/per_page. get_pipeline_logs exposes max_lines with truncation logic (pipelines.py:92-94, 158-161). list_pipelines exposes 8 filter params total.
6 PR body matches template FIXED PR body has all 5 required sections: Summary, Changes, Test Plan, Review Checklist, Related Notes.

Round 2 nits (3/3):

# Nit Status Evidence
1 list_repos should have active/page/per_page params FIXED list_repos(active, page, per_page) at repos.py:13-16, passed through to SDK at line 24.
2 PR body Related Notes should include both issue slug AND plan slug FIXED PR body references both issue-woodpecker-mcp-base-server and plan-2026-02-28-woodpecker-mcp.
3 list_pipelines should expose before/after/ref params FIXED All three params exposed at pipelines.py:222-228 and passed to SDK at lines 240-244.

CODE QUALITY ASSESSMENT

Architecture compliance:

  • FastMCP pattern correctly implemented: mcp = FastMCP("woodpecker") singleton in server.py
  • get_client() is patchable: all 6 tool modules import get_client from ..server, never use _client directly
  • _ok()/_error_response() helpers used consistently across all 25 tools
  • lookup_repo() centralizes name-to-ID resolution
  • register_all_tools() imports all 6 domain modules
  • __init__.py exports mcp via __all__ for mcp-remote compatibility

All 25 tools present:

  • pipelines.py: 6 tools (trigger, status, logs, restart, cancel, list)
  • repos.py: 2 tools (list_repos, get_repo)
  • secrets.py: 8 tools (4 repo + 4 global CRUD)
  • crons.py: 5 tools (list, create, update, delete, run)
  • system.py: 3 tools (healthz, get_version, get_current_user)
  • queue.py: 1 tool (get_queue_status)

Response curation:

  • No raw SDK passthrough anywhere. Every tool selects specific fields via .get() calls.
  • Consistent pattern: try/except with _ok(curated_dict) or _error_response(exc).

Test coverage:

  • 19 tests across 6 test files covering all tool domains
  • Proper conftest.py with session-scoped fixtures
  • pytest.skip() used correctly for expected failures (missing credentials, API errors)
  • CRUD lifecycle tests for secrets and crons (create -> list -> verify -> update -> delete)
  • Mutating pipeline tests (trigger, cancel, restart) all present

Code consistency:

  • All tool modules use identical import pattern: from ..server import _error_response, _ok, get_client, lookup_repo, mcp
  • Annotated types with Field(description=...) used consistently for all parameters
  • No scattered type conversions

SOP COMPLIANCE

  • Branch named after issue: 1-base-mcp-server-with-25-workflow-tools (references issue #1)
  • PR body follows template-pr-body: all 5 sections present (Summary, Changes, Test Plan, Review Checklist, Related Notes)
  • Related Notes references both issue slug (issue-woodpecker-mcp-base-server) and plan slug (plan-2026-02-28-woodpecker-mcp)
  • Tests exist and cover all domains (19 tests, 6 test files)
  • No secrets, .env files, or credentials committed (.env in .gitignore, credentials loaded from ~/secrets/ at runtime)
  • No unnecessary file changes (all files directly support the 25-tool MCP server)
  • Commit messages are descriptive (3 commits: feat, fix, fix -- clear progression)
  • No scope creep

VERDICT: APPROVED

All 9 previous nits resolved. Zero new issues found. Code quality is high -- clean architecture, consistent patterns, proper error handling, curated responses, comprehensive tests. Ready for merge.

## PR #2 Review (Round 3 -- Fresh QA) ### BLOCKERS None. ### NITS None. ### PREVIOUS NIT VERIFICATION (9/9 FIXED) **Round 1 nits (6/6):** | # | Nit | Status | Evidence | |---|-----|--------|----------| | 1 | `get_version` and `get_queue_info` should return curated responses | FIXED | `get_version()` returns only `version`/`source` (system.py:29-33). `get_queue_status()` curates into `paused`, `worker_count`, `running_count`, `pending_count`, `waiting_on_deps_count` (queue.py:35-42). | | 2 | `cron_id` int-to-str should be clean (single conversion) | FIXED | `cron_id_str = str(cron_id)` is a single conversion at point of use in all three cron tools (crons.py:77, 99, 114). | | 3 | Test error handling should use `pytest.skip()`, not silent return | FIXED | `test_trigger_pipeline`, `test_cancel_pipeline`, `test_restart_pipeline` all use `pytest.skip()` for API errors (test_pipelines.py:62-63, 73-74, 86-87). | | 4 | Tests should exist for `trigger_pipeline`, `restart_pipeline`, `cancel_pipeline` | FIXED | All three have dedicated tests (test_pipelines.py:59-89). `test_cancel_pipeline` even triggers a pipeline first to have something to cancel. | | 5 | SDK filter params exposed (pagination on list tools, max_lines on logs) | FIXED | All list tools expose `page`/`per_page`. `get_pipeline_logs` exposes `max_lines` with truncation logic (pipelines.py:92-94, 158-161). `list_pipelines` exposes 8 filter params total. | | 6 | PR body matches template | FIXED | PR body has all 5 required sections: Summary, Changes, Test Plan, Review Checklist, Related Notes. | **Round 2 nits (3/3):** | # | Nit | Status | Evidence | |---|-----|--------|----------| | 1 | `list_repos` should have `active`/`page`/`per_page` params | FIXED | `list_repos(active, page, per_page)` at repos.py:13-16, passed through to SDK at line 24. | | 2 | PR body Related Notes should include both issue slug AND plan slug | FIXED | PR body references both `issue-woodpecker-mcp-base-server` and `plan-2026-02-28-woodpecker-mcp`. | | 3 | `list_pipelines` should expose `before`/`after`/`ref` params | FIXED | All three params exposed at pipelines.py:222-228 and passed to SDK at lines 240-244. | ### CODE QUALITY ASSESSMENT **Architecture compliance:** - FastMCP pattern correctly implemented: `mcp = FastMCP("woodpecker")` singleton in server.py - `get_client()` is patchable: all 6 tool modules import `get_client` from `..server`, never use `_client` directly - `_ok()`/`_error_response()` helpers used consistently across all 25 tools - `lookup_repo()` centralizes name-to-ID resolution - `register_all_tools()` imports all 6 domain modules - `__init__.py` exports `mcp` via `__all__` for mcp-remote compatibility **All 25 tools present:** - pipelines.py: 6 tools (trigger, status, logs, restart, cancel, list) - repos.py: 2 tools (list_repos, get_repo) - secrets.py: 8 tools (4 repo + 4 global CRUD) - crons.py: 5 tools (list, create, update, delete, run) - system.py: 3 tools (healthz, get_version, get_current_user) - queue.py: 1 tool (get_queue_status) **Response curation:** - No raw SDK passthrough anywhere. Every tool selects specific fields via `.get()` calls. - Consistent pattern: try/except with `_ok(curated_dict)` or `_error_response(exc)`. **Test coverage:** - 19 tests across 6 test files covering all tool domains - Proper `conftest.py` with session-scoped fixtures - `pytest.skip()` used correctly for expected failures (missing credentials, API errors) - CRUD lifecycle tests for secrets and crons (create -> list -> verify -> update -> delete) - Mutating pipeline tests (trigger, cancel, restart) all present **Code consistency:** - All tool modules use identical import pattern: `from ..server import _error_response, _ok, get_client, lookup_repo, mcp` - Annotated types with `Field(description=...)` used consistently for all parameters - No scattered type conversions ### SOP COMPLIANCE - [x] Branch named after issue: `1-base-mcp-server-with-25-workflow-tools` (references issue #1) - [x] PR body follows `template-pr-body`: all 5 sections present (Summary, Changes, Test Plan, Review Checklist, Related Notes) - [x] Related Notes references both issue slug (`issue-woodpecker-mcp-base-server`) and plan slug (`plan-2026-02-28-woodpecker-mcp`) - [x] Tests exist and cover all domains (19 tests, 6 test files) - [x] No secrets, .env files, or credentials committed (`.env` in `.gitignore`, credentials loaded from `~/secrets/` at runtime) - [x] No unnecessary file changes (all files directly support the 25-tool MCP server) - [x] Commit messages are descriptive (3 commits: feat, fix, fix -- clear progression) - [x] No scope creep ### VERDICT: APPROVED All 9 previous nits resolved. Zero new issues found. Code quality is high -- clean architecture, consistent patterns, proper error handling, curated responses, comprehensive tests. Ready for merge.
Sign in to join this conversation.
No description provided.