Plan: Woodpecker MCP Server
Vision
Professional, stable MCP tooling for every platform service, built on swagger-generated SDKs with pytest integration tests and PyPI pipelines. Eliminate raw API calls from agent sessions.
Projects & Repos Touched
| Project/Repo | Platform | Role in this plan |
|---|---|---|
| woodpecker-mcp (new) | Forgejo | Base MCP server with workflow-level tools built on woodpecker-sdk (stdio transport) |
| woodpecker-mcp-remote (new, Phase 2) | Forgejo | Streamable HTTP wrapper enabling claude.ai access |
| woodpecker-sdk | Forgejo | Dependency — Python SDK providing 117 endpoints |
Context
The woodpecker-sdk is complete (117 endpoints, 18 mixins, 70 integration tests, PR #2 and #4 merged in parent plan, v0.1.0 published to Forgejo PyPI). Agents currently make raw HTTP calls to Woodpecker CI, wasting 500-1000 tokens per interaction with no type safety, no error handling, and no retry logic. This plan builds the MCP server that wraps the SDK into workflow-level tools that agents, skills, and other MCP consumers can use directly.
The paradigm: The SDK is a faithful, complete 1:1 wrapper over every Woodpecker API endpoint (117 endpoints). The MCP is the expertise layer — it composes those SDK calls into tools that encode how Woodpecker should be used: resolving repo names to IDs, fetching context before acting, returning curated results. An agent using the MCP shouldn't need to understand Woodpecker's internal data model. Agents wrap skills, skills wrap MCP endpoints, MCP endpoints wrap SDK calls.
Reference implementations:
forgejo-mcp(~/forgejo-mcp/) — FastMCP server with 11 workflow tools. Same base architecture.gcal-mcp+gcal-mcp-remote— The stdio + Streamable HTTP dual pattern. Base MCP defines tools, remote wrapper patchesget_client()with ContextVar for per-request isolation and adds OAuth.mcp-remote-auth(~/mcp-remote-auth/) — Shared OAuth infrastructure: OAuthProxyProvider, TokenStore, auth middleware.pal-e-docs-mcp,notion-mcp— Larger MCPs that split tools into multiple domain files.
What's already done:
- [x] woodpecker-sdk complete — 117 endpoints, 18 mixins (plan-2026-02-28-woodpecker-sdk-mcp Phase 1)
- [x] Integration tests — 70 tests, response shape validation
- [x] SDK published to Forgejo PyPI (v0.1.0)
- [x] CI pipeline for woodpecker-sdk (lint, test, publish)
- [x] forgejo-mcp pattern studied and documented
- [x] mcp-remote pattern studied — gcal-mcp-remote, gmail-mcp-remote, mcp-remote-auth analyzed
- [x] Tool design: 25 focused tools across 6 domain files
- [x] SDK endpoint surface fully mapped and verified against plan
- [x] woodpecker-mcp repo created (Phase 1a)
- [x] Phase 1a PR #2 submitted — 25 tools, 19 integration tests, registered in ~/.mcp.json
- [x] Phase 1a PR #2 reviewed and merged — 3 QA rounds, 9 nits resolved, squash-merged 2026-03-01
- [ ] Phase 1b: activate_repo tool added
- [ ] woodpecker-mcp-remote repo created
Previous Plan
plan-2026-02-28-woodpecker-sdk-mcp — Phase 3 promoted to this planDepends On
phase-2026-02-28-2-pypi-pipeline (parent plan) — ✅ SATISFIED. SDK v0.1.0 published to Forgejo PyPI.Decisions Made
| Decision | Rationale |
|---|---|
| Follow forgejo-mcp architecture exactly | FastMCP + @mcp.tool() decorators. server.py (app, lazy singleton client, _ok()/_error_response() helpers) + tools/*.py. Proven pattern across forgejo, notion, gmail, gcal, pal-e-docs MCPs. |
| Repo name as primary input, not integer IDs | Agents think in repo names ("forgejo_admin/my-repo"), not Woodpecker's internal integer IDs. Every tool resolves name → ID internally via <code>lookup_repo()</code>. Optional <code>repo_id</code> override for advanced/scripted use. |
| Separate focused tools, not compound "manage" tools | Each CRUD operation gets its own tool (list_repo_secrets, create_repo_secret, etc.) instead of a single "manage_repo_secret" with an action parameter. Clearer MCP schema, self-documenting parameters, matches forgejo-mcp pattern. Results in 25 tools instead of 12, but each tool has a focused description and obvious parameters. |
| Smart log fetching | <code>get_pipeline_logs</code> takes repo name + pipeline number, automatically fetches the pipeline to discover steps, and returns logs for the failed step by default (or all steps, or a named step). Eliminates the two-call dance agents would otherwise need. |
| Split tools by domain (6 files) | 25 tools is too many for a single workflows.py. Split into pipelines.py, repos.py, secrets.py, crons.py, system.py, queue.py. Matches the pattern in larger MCPs (notion-mcp, gcal-mcp, pal-e-docs-mcp). |
| get_client() must be patchable | All tool modules import <code>get_client</code> from server.py — never use the global <code>_client</code> directly. This is what makes the mcp-remote wrapper work: it replaces <code>get_client</code> in all tool modules before they're imported. Critical for Phase 2 (mcp-remote). |
| Pull system info tools into Tier 1 | healthz, get_version, get_current_user are zero-parameter one-liners — trivially useful for agents checking CI status. No reason to defer. |
| Separate repo: woodpecker-mcp | Consistent with forgejo-sdk / forgejo-mcp split. SDK and MCP have different dependency trees and release cadences. |
| Development with editable installs | During development, <code>pip install -e ../woodpecker-sdk</code>. Production pyproject.toml depends on published <code>ldraney-woodpecker-sdk>=0.1.0</code>. |
| mcp-remote is Phase 2, not separate plan | Woodpecker remote is simpler than Google/Notion (single PAT, no per-user OAuth). Keeps the full MCP lifecycle in one plan. Remote wrapper is a separate repo but same plan. |
| Pull activate_repo into Phase 1b | Real-world agent transcript showed an agent spending ~8 bash calls (3 failures) to activate repos via raw API. Without activate_repo, agents must cross-reference Forgejo API for forge_remote_id, then construct POST to Woodpecker. The MCP tool accepts a Forgejo repo full name and handles ID resolution internally. Added as Phase 1b to avoid blocking Phase 1a PR review. |
| MCP registered in global ~/.mcp.json | The woodpecker MCP is registered in the global <code>~/.mcp.json</code> file (not ~/.claude/mcp.json). Enabled via <code>enabledMcpjsonServers</code> in settings.local.json. Uses <code>uv run --directory ~/woodpecker-mcp</code> with WOODPECKER_URL and WOODPECKER_TOKEN env vars. |
Architecture
Base MCP Pattern (Phase 1 — stdio transport)
Key patterns (verified across 5+ existing MCPs):
- Module-level
mcp = FastMCP("woodpecker")singleton - Lazy client via global
_client+get_client()reading WOODPECKER_URL and WOODPECKER_TOKEN from environment - All tool modules import
get_clientfrom server.py (patchable for mcp-remote) __init__.pyexportsmcpvia__all__(allows remote wrapper to import it)- All tools return
str(JSON-formatted) via_ok()helper - All tools have try/except returning
_error_response(exc)for HTTP errors - Parameters use
Annotated[type, Field(description="...")]from pydantic - Response shaping: tools return curated fields, not raw API dumps
- Registered in
~/.mcp.jsonviauv run --directory, enabled in settings.local.json
Remote Wrapper Pattern (Phase 2 — Streamable HTTP)
Key pattern: The remote wrapper imports the FastMCP instance from the base MCP, patches
get_client() with a ContextVar-based version for per-request isolation, configures auth, and runs with transport="streamable-http". The base MCP knows nothing about HTTP or auth.Woodpecker simplification: Unlike Google/Notion MCPs which need three-party OAuth (each user has their own credentials), Woodpecker uses a single admin PAT. The remote wrapper is simpler — it injects the server-side PAT into per-request clients rather than managing per-user OAuth flows. Auth for the remote endpoint itself can use a simple API key or the mcp-remote-auth pattern with a lightweight provider.
Proposed MCP Tools (Phase 1a — 25 tools)
Pipeline tools (6) — tools/pipelines.py
| Tool name | SDK calls composed | Description |
|---|---|---|
| <code>trigger_pipeline</code> | lookup_repo + create_pipeline | Trigger a build by repo name + branch, with optional variables |
| <code>get_pipeline_status</code> | lookup_repo + get_pipeline | Get pipeline status, steps, timing by repo name + pipeline number |
| <code>get_pipeline_logs</code> | lookup_repo + get_pipeline + get_step_logs | Fetch logs for a pipeline. Auto-discovers steps; returns failed step by default, or all steps, or a specific step name. Supports max_lines truncation. |
| <code>restart_pipeline</code> | lookup_repo + restart_pipeline | Restart a pipeline by repo name + number |
| <code>cancel_pipeline</code> | lookup_repo + cancel_pipeline | Cancel a running pipeline by repo name + number |
| <code>list_pipelines</code> | lookup_repo + list_pipelines | List recent pipelines with filtering (branch, status, event, before, after, ref, page, per_page) |
Repo tools (2) — tools/repos.py
| Tool name | SDK calls composed | Description |
|---|---|---|
| <code>list_repos</code> | list_repos (direct) | List all Woodpecker-activated repos with optional active/page/per_page filters |
| <code>get_repo</code> | lookup_repo or get_repo | Get repo config and status by name or ID |
Repo secret tools (4) — tools/secrets.py
| Tool name | SDK calls composed | Description |
|---|---|---|
| <code>list_repo_secrets</code> | lookup_repo + list_repo_secrets | List secrets for a repo (names only, values are masked) with page/per_page |
| <code>create_repo_secret</code> | lookup_repo + create_repo_secret | Create a new secret on a repo with name, value, and event filters |
| <code>update_repo_secret</code> | lookup_repo + update_repo_secret | Update an existing repo secret's value or event filters |
| <code>delete_repo_secret</code> | lookup_repo + delete_repo_secret | Delete a repo secret by name |
Global secret tools (4) — tools/secrets.py
| Tool name | SDK calls composed | Description |
|---|---|---|
| <code>list_global_secrets</code> | list_global_secrets (direct) | List all global secrets (names only, values are masked) with page/per_page |
| <code>create_global_secret</code> | create_global_secret (direct) | Create a new global secret |
| <code>update_global_secret</code> | update_global_secret (direct) | Update a global secret's value or event filters |
| <code>delete_global_secret</code> | delete_global_secret (direct) | Delete a global secret by name |
Cron tools (5) — tools/crons.py
| Tool name | SDK calls composed | Description |
|---|---|---|
| <code>list_cron_jobs</code> | lookup_repo + list_cron_jobs | List cron jobs for a repo with page/per_page |
| <code>create_cron_job</code> | lookup_repo + create_cron_job | Create a cron job with name, schedule, and optional branch |
| <code>update_cron_job</code> | lookup_repo + update_cron_job | Update a cron job's schedule or branch |
| <code>delete_cron_job</code> | lookup_repo + delete_cron_job | Delete a cron job by name |
| <code>run_cron_job</code> | lookup_repo + run_cron_job | Manually trigger a cron job |
System tools (3) — tools/system.py
| Tool name | SDK calls composed | Description |
|---|---|---|
| <code>healthz</code> | healthz (direct) | Check if Woodpecker server is healthy |
| <code>get_version</code> | get_version (direct) | Get Woodpecker server version (curated: version + source) |
| <code>get_current_user</code> | get_current_user (direct) | Get the authenticated user's info (curated: id, login, email, admin, active) |
Queue tools (1) — tools/queue.py
| Tool name | SDK calls composed | Description |
|---|---|---|
| <code>get_queue_status</code> | get_queue_info + list_queued_pipelines | Overview of what's running and waiting (curated: stats + pipeline summaries) |
Phase 1b Tool — activate_repo
| Tool name | SDK calls composed | Description |
|---|---|---|
| <code>activate_repo</code> | activate_repo(forge_remote_id=...) | Activate a Forgejo repo in Woodpecker CI. Accepts a <code>forge_remote_id</code> (the Forgejo repo's integer ID). Returns the activated Woodpecker repo with its new ID and config. |
Rationale: Real-world agent transcript showed ~8 raw API calls (3 failures) to activate repos. The SDK's
activate_repo(forge_remote_id=str) method handles this in one call. The MCP tool wraps it cleanly. Note: the agent still needs the Forgejo repo ID (from forgejo-mcp's get_repo), but the Woodpecker-side complexity is eliminated.Research: SDK Endpoint Surface (117 endpoints, 18 mixins)
Tier 1 — Covered in Phase 1a+1b (26 tools)
| Mixin | Count | Key methods used by MCP tools |
|---|---|---|
| Pipelines | 10 | list_pipelines, create_pipeline, get_pipeline, restart_pipeline, cancel_pipeline (5 of 10 used directly) |
| PipelineLogs | 4 | get_step_logs (1 of 4 — delete/stream not needed for Tier 1) |
| Repositories | 13 | list_repos, lookup_repo, get_repo, activate_repo (4 of 13) |
| RepoSecrets | 5 | All 5: list, create, get, delete, update |
| Secrets (global) | 5 | All 5: list, create, get, delete, update |
| CronJobs | 6 | All 6: list, create, get, run, delete, update |
| PipelineQueues | 5 | get_queue_info, list_queued_pipelines (2 of 5) |
| System | 6 | healthz, get_version (2 of 6) |
| User | 5 | get_current_user (1 of 5) |
Tier 2 — Phase 4 (admin tools, future)
| Mixin | Count | Key methods |
|---|---|---|
| Agents | 10 | global + org-scoped agent CRUD, list_agent_tasks |
| Organizations | 15 | org CRUD, org secrets/registries/agents/permissions |
| Users (admin) | 5 | admin user CRUD |
| Forges | 5 | CRUD forges |
| RepoRegistries | 5 | CRUD repo registries |
| Registries (global) | 5 | CRUD global registries |
| Remaining Pipeline ops | 5 | approve, decline, delete, get_config, get_metadata |
| Remaining Repo ops | 9 | delete, update, move, chown, branches, PRs, permissions, repair, repair_all |
| Remaining Queue ops | 3 | pause, resume, wait_for_running |
Tier 3 — Skip (not useful via MCP)
| Mixin | Count | Reason to skip |
|---|---|---|
| Debug | 10 | pprof endpoints — binary profile data, not useful to agents |
| Badges | 2 | SVG/XML output — not useful to agents |
| Events | 1 | SSE streaming — doesn't fit MCP request/response model |
Phases
Phase 1a — Base MCP server with 25 workflow tools ✅ COMPLETED
Slug:
Goal: Create woodpecker-mcp repo with FastMCP server, 25 focused workflow tools across 6 domain files, integration tests, and register in Claude config. Design for mcp-remote compatibility (patchable get_client).
Owner: Agent
Issue:
PR: #2 — squash-merged 2026-03-01
phase-2026-02-28-1a-mcp-baseGoal: Create woodpecker-mcp repo with FastMCP server, 25 focused workflow tools across 6 domain files, integration tests, and register in Claude config. Design for mcp-remote compatibility (patchable get_client).
Owner: Agent
Issue:
issue-woodpecker-mcp-base-serverPR: #2 — squash-merged 2026-03-01
Deliverables:
- 25 tools across 6 domain modules (pipelines, repos, secrets, crons, system, queue)
- 19 integration tests with proper pytest patterns
- All responses curated (no raw SDK passthrough)
- All list tools expose pagination (page/per_page)
- list_pipelines exposes 8 filter params (branch, status, event, before, after, ref, page, per_page)
- get_pipeline_logs supports max_lines truncation
- get_client() patchable — confirmed via QA (no tool module accesses _client directly)
- Registered in ~/.mcp.json, enabled via enabledMcpjsonServers in settings.local.json
- 3 QA rounds, 9 nits resolved before merge
Steps:
- ✅ Create
woodpecker-mcprepo on Forgejo - ✅ Scaffold server following verified pattern
- ✅ Implement repo name → ID resolution helper
- ✅ Implement 6 pipeline tools (tools/pipelines.py)
- ✅ Implement 2 repo tools (tools/repos.py)
- ✅ Implement 8 secret tools (tools/secrets.py)
- ✅ Implement 5 cron tools (tools/crons.py)
- ✅ Implement 3 system tools (tools/system.py)
- ✅ Implement 1 queue tool (tools/queue.py)
- ✅ Integration tests (19 tests passing)
- ✅ Register in ~/.mcp.json, enable in settings.local.json
- ✅ QA review (3 rounds, 9 nits resolved)
- ✅ Merge PR #2
Phase 1b — Add activate_repo tool
Slug:
Goal: Add
Owner: Agent
Issue: TBD (create when starting)
Depends on: Phase 1a ✅
phase-2026-02-28-1b-activate-repoGoal: Add
activate_repo tool to tools/repos.py, bringing the total to 26 tools. Closes the gap exposed by real-world agent transcript where repo activation via raw API required ~8 calls with multiple failures.Owner: Agent
Issue: TBD (create when starting)
Depends on: Phase 1a ✅
Steps:
- Add
activate_repoto tools/repos.py (acceptsforge_remote_id: str) - Add integration test
- Update register_all_tools if needed
- Open PR, review, merge
Phase 2 — Streamable HTTP remote wrapper (claude.ai access)
Slug:
Goal: Create woodpecker-mcp-remote repo that wraps the base MCP over Streamable HTTP, enabling claude.ai to connect to Woodpecker CI tools.
Owner: Agent
Issue: TBD
Depends on: Phase 1a ✅
phase-2026-02-28-2-mcp-remoteGoal: Create woodpecker-mcp-remote repo that wraps the base MCP over Streamable HTTP, enabling claude.ai to connect to Woodpecker CI tools.
Owner: Agent
Issue: TBD
Depends on: Phase 1a ✅
Context: The mcp-remote pattern is proven across gcal-mcp-remote, gmail-mcp-remote, and notion-mcp-remote. Woodpecker is simpler than those because it uses a single admin PAT (no per-user OAuth). The remote wrapper imports the base MCP's FastMCP instance, patches
get_client() with ContextVar-based per-request isolation, configures auth, and serves over Streamable HTTP.Key decisions (to be made when starting):
- Auth strategy for the remote endpoint — simple API key, or mcp-remote-auth with lightweight provider? (Woodpecker PAT is server-side, not per-user)
- Deployment target — Kubernetes on pal-e cluster, or systemd on dev machine?
- Network exposure — Tailnet-only, or public with auth?
- Whether to use mcp-remote-auth shared library or build simpler bespoke auth
Steps:
- Create
woodpecker-mcp-remoterepo on Forgejo - Implement client_patch.py (ContextVar-based get_client replacement)
- Implement server.py (import base mcp, apply patch, configure auth + transport)
- Add /health endpoint
- Configure deployment (systemd service or K8s manifest)
- Test from claude.ai — verify tool discovery and invocation
Phase 3 — Tier 2 admin tools (future)
Slug:
Goal: Add admin/org/queue/user management tools to woodpecker-mcp base.
Owner: Agent
Issue: TBD
phase-2026-02-28-3-mcp-tier2Goal: Add admin/org/queue/user management tools to woodpecker-mcp base.
Owner: Agent
Issue: TBD
Key Files
| Phase | File | Repo | Change |
|---|---|---|---|
| 1a | src/woodpecker_mcp/server.py | woodpecker-mcp | FastMCP app, WoodpeckerClient lifecycle, _ok()/_error_response(), get_client() (patchable) |
| 1a | src/woodpecker_mcp/tools/pipelines.py | woodpecker-mcp | 6 pipeline tools including smart log fetching with max_lines |
| 1a | src/woodpecker_mcp/tools/repos.py | woodpecker-mcp | 2 repo tools (list with filters, get) |
| 1a | src/woodpecker_mcp/tools/secrets.py | woodpecker-mcp | 8 secret tools (repo + global CRUD) with pagination |
| 1a | src/woodpecker_mcp/tools/crons.py | woodpecker-mcp | 5 cron tools with pagination and clean cron_id handling |
| 1a | src/woodpecker_mcp/tools/system.py | woodpecker-mcp | 3 system/info tools with curated responses |
| 1a | src/woodpecker_mcp/tools/queue.py | woodpecker-mcp | 1 queue status tool with curated stats |
| 1a | src/woodpecker_mcp/tools/__init__.py | woodpecker-mcp | register_all_tools() imports all 6 tool modules |
| 1a | src/woodpecker_mcp/__init__.py | woodpecker-mcp | Package init, exports mcp via __all__ |
| 1a | src/woodpecker_mcp/__main__.py | woodpecker-mcp | python -m runner |
| 1a | pyproject.toml | woodpecker-mcp | Package config: deps on mcp + woodpecker-sdk, console script |
| 1a | tests/ | woodpecker-mcp | 19 integration tests against live Woodpecker |
| 1b | src/woodpecker_mcp/tools/repos.py | woodpecker-mcp | Add activate_repo tool (3 tools total in repos.py) |
| 2 | src/woodpecker_mcp_remote/server.py | woodpecker-mcp-remote | Import base mcp, configure auth + Streamable HTTP transport |
| 2 | src/woodpecker_mcp_remote/client_patch.py | woodpecker-mcp-remote | ContextVar-based get_client() replacement |
| 2 | pyproject.toml | woodpecker-mcp-remote | Deps on woodpecker-mcp + mcp-remote-auth + uvicorn |
Verification
Phase 1a ✅
- [x]
pip install -e .works with local SDK dep - [x] All 25 tools callable from MCP client
- [x]
trigger_pipelineworks by repo name (resolves name → ID internally) - [x]
get_pipeline_logsauto-discovers steps and returns failed step logs - [x] Secret CRUD works (create, list, update, delete) for both repo and global
- [x] Cron CRUD works (create, list, update, delete, run)
- [x] System tools work (healthz, get_version, get_current_user)
- [x] Registered in ~/.mcp.json — agent session can see and call tools
- [x] pytest passes against live Woodpecker instance (19 tests)
- [x] get_client() is patchable (all tool modules import it from server.py)
- [x] All responses curated (no raw SDK passthrough)
- [x] All list tools expose pagination
- [x] QA clean pass (3 rounds, 9 nits resolved)
Phase 1b
- [ ]
activate_repoworks with forge_remote_id - [ ] Integration test passes
Phase 2
- [ ] Remote wrapper imports base MCP's FastMCP instance
- [ ] client_patch.py replaces get_client() with ContextVar version
- [ ] Streamable HTTP transport works
- [ ] /health endpoint responds
- [ ] claude.ai can discover and call all tools
- [ ] Auth prevents unauthorized access
Next Plan Seeds
- Skills wrapping woodpecker-mcp tools (e.g.,
/deploy,/ci-status) - Enhanced
/review-prskill that checks Woodpecker CI status - forgejo-mcp audit against this pattern (
todo-forgejo-mcp-audit) - PyPI pipeline for woodpecker-mcp (once SDK pipeline pattern is proven across repos)
- Tier 2 admin tools (Phase 3 of this plan)
Related
plan-2026-02-28-woodpecker-sdk-mcp— parent plan (SDK + PyPI pipeline) ✅ COMPLETEDplan-2026-03-01-forgejo-pypi-migration— PyPI pipeline pattern for all Python repostodo-woodpecker-mcp— originating TODO ✅ DONEtodo-forgejo-mcp-audit— companion audit- forgejo-mcp (
~/forgejo-mcp/) — base MCP reference - gcal-mcp + gcal-mcp-remote (
~/calendar-mcp/,~/gcal-mcp-remote/) — mcp-remote reference - mcp-remote-auth (
~/mcp-remote-auth/) — shared OAuth infrastructure - forgejo-sdk (
~/forgejo-sdk/) — SDK reference