API Compatibility

What this answers: “will this madtea feature work against my server?” — madtea features × the Gitea and Forgejo REST APIs — plus the maintainer view: tracking drift between the two platforms. The client-vs-client question (“which CLI covers what”) is a different axis and lives in COMPARISON.md.

madtea targets Gitea API v1 as its single API surface (ADR 0017); Forgejo is supported through the Gitea-compatible API it maintains. Where Forgejo diverges, madtea detects the platform and surfaces the gap as a typed, explained error — see How madtea handles divergence.

This is a matrix, not a manual: Notes cells name endpoints and platform caveats in a clause or two. Semantics live in docs/reference/ and madt_help; a size test caps row length to keep it that way.

Legend: ✅ supported · ❌ not supported · ⚠️ partial — see notes · ⚙️ endpoint exists in spec but is feature-flag gated and off by default Sources of truth. Gitea API: docs/api-specs/gitea-1.26.1.json. Forgejo API: docs/api-specs/forgejo-15.0.json plus the empirical 404 probe in forgejo-15-actions-probe.md. When the spec and the probe disagree on Forgejo (the spec lists endpoints that 404 at runtime), the probe wins.

API divergence last probed: 2026-06-18.


Command availability by platform

Any madtea command not listed here works identically on both platforms. The divergent set:

CommandGitea 1.26Forgejo 15.0Notes
pr cancel-mergeForgejo has no DELETE /pulls/{n}/merge route.
file contents (bulk) / file get-ext⚠️/file-contents and /contents-ext/{path} are Gitea-only (MCP madt_files action=contents); on Forgejo madtea degrades to plain /contents reads (per-file for bulk), so both still return content.
actions run-delete405 on Forgejo.
actions jobs / job / job-logs / logs/actions/jobs/... family 404s on Forgejo.
actions rerun / rerun-jobForgejo: web UI only.
actions artifacts404 on Forgejo.
actions workflows (list/get/enable/disable)404 on Forgejo. actions dispatch works on both — Forgejo requires Content-Type: application/json.
admin runners⚠️Top-level /admin/runners absent on Forgejo (only /admin/actions/runners); madtea falls back gracefully.
repo flags⚙️/repos/{r}/flags config-gated by [repository] ENABLE_FLAGS=true; 404 on default installs.
user quota⚙️/user/quota config-gated by [quota] ENABLED=true; 404 on default installs. Gitea has no /quota family.
activitypub⚙️Config-gated by [federation] ENABLED=true; 404 on default installs. Gitea has no /activitypub paths.
project⚠️⚠️Project-board settings only (has_projects, projects_mode); board CRUD is web-UI-only on both.
signing-keyForgejo additionally serves /signing-key.ssh.
setup --web⚠️Browser OAuth login works on both; the final PAT mint (POST /users/{username}/tokens with the OAuth token as the basic-auth password) is rejected by Forgejo (auth method not allowed), so madtea falls back to token paste. Empirically 401 on Forgejo 15.0.1 stable (matches source-verified Forgejo main).

How madtea handles divergence

Flavour is detected by probing /api/v1/version on the first request that needs it: Forgejo’s +gitea-X suffix (15.0.1+gitea-1.22.0) is unique, and a set X-Forgejo-Version response header is an unambiguous Forgejo marker even without the suffix; Gitea returns a bare 1.26.1 with no such header. Falls back to /nodeinfo (software.name) when /version is unavailable. The result is cached per forge host, process-wide (one probe per host across all clients), and exposed via gitea.ServerFlavourClient.

madtea handles a detected divergence two ways. Where a Gitea-only endpoint has a plain equivalent, the call degrades gracefully: file get-ext (/contents-ext) and file contents (/file-contents) fall back to plain /contents reads on Forgejo. Where no equivalent exists, a bare-router 404 page not found for a confirmed-unimplemented endpoint family is wrapped as *gitea.ErrUnsupportedOnForgejo (preserves the underlying *APIError, so gitea.IsNotFound still holds) instead of a generic API error 404. A JSON-body {"message": "..."} 404 is a resource-missing 404 (e.g. runner ID 9999), not endpoint-missing, and passes through untouched.

Example surfaced message:

endpoint not implemented on Forgejo (server=15.0.1+gitea-1.22.0):
actions/workflows (list) — request was /repos/o/r/actions/workflows

Re-verify the whole picture (fresh forgejo:15 container, admin token, smoke battery):

bash tests/integration/check-drift.sh smoke

Per-endpoint re-probing: forgejo-15-actions-probe.md.

Gitea vs Forgejo API differences

The per-endpoint picture behind the command table above:

FeatureGitea 1.26Forgejo 15.0Notes
PR list base_branchParam accepted but ignored on Forgejo.
PR create reviewersField accepted but ignored on Forgejo.
PR create team_reviewers
Issue list sort param
Issue list labels= filter — label namesBoth platforms accept a comma-separated list of label NAMES in labels=; numeric IDs are treated as unknown names and silently discarded. madtea passes names (resolving any user-supplied IDs to their names).
Issue list labels= filter — org label namesGitea silently discards org-level label names in labels= (filter opens); Forgejo resolves them. madtea passes names through unchanged; org labels work on Forgejo, not on Gitea (platform limitation).
Issue list assigned_by filterThe per-repo issue list uses assigned_by on both platforms (not assignee). madtea maps --assignee / assignee= to assigned_by.
PR list labels= filter — label/org-label namesThe /pulls labels= param takes numeric label IDs (array) on both platforms, not names — a name silently opens the filter (returns all PRs). madtea resolves label names (repo and org) to IDs, so name filtering works on both.
Cross-repo issue search labels= filter — org label names/repos/issues/search resolves label names globally (org labels included) on both platforms, so org-label names filter correctly with no ID resolution; madtea passes names through unchanged (a numeric ID would be read as a name and discarded). Nonexistent names are silently dropped by the server.
Repo create object_format
Actions runs list/getGET /actions/runs and GET /actions/runs/{id} work on both.
Actions runs delete (DELETE /actions/runs/{id})Returns 405 on Forgejo (endpoint is GET-only).
Actions runs/{id}/jobsGET /actions/runs/{id}/jobs returns 404 page not found on Forgejo.
Actions runs/{id}/cancel (POST)Forgejo: web UI only (no REST endpoint).
Actions runs/{id}/rerun (POST)Forgejo: web UI only (no REST endpoint).
Actions runs/{id}/artifacts/actions/runs/{id}/artifacts is 404 on Forgejo.
Actions runs/{id}/logs/actions/runs/{id}/logs is 404 on Forgejo.
Actions workflows (list / get / enable / disable)/actions/workflows family absent on Forgejo.
Actions workflows/{wf}/dispatches (POST)Works on Forgejo with Content-Type: application/json.
Actions jobs (/actions/jobs/{id}, /logs)Repo-scoped jobs endpoints absent on Forgejo.
Actions runners (repo / user / admin / org scope)All work on Forgejo 15 (contrary to prior reports against Forgejo 11).
Actions runners/registration-token (POST)Forgejo returns 405 — endpoint is GET-only. madtea currently posts; tracked.
Actions artifacts (list at /actions/artifacts)Endpoint absent on Forgejo.
Actions secrets, variables, tasks
/user/actions/runs, /user/actions/jobsCross-cutting “all my runs/jobs” only on Gitea.
/admin/actions/runs, /admin/actions/jobsCluster-wide views only on Gitea — Forgejo has /admin/runners/jobs for active jobs.
/admin/runners (top-level)Forgejo exposes only /admin/actions/runners.
/admin/runners/jobsReturns active runner jobs on both.
Actions runs/jobs (non-owner)Fixed in Gitea 1.26 (issue 36818).
/file-contents (bulk file fetch)⚠️Gitea-only route; on Forgejo madtea falls back to per-file /contents/{path} reads (same array shape, reads default branch).
/contents-ext/{path} (extended-metadata file fetch)⚠️Gitea-only route; on Forgejo madtea falls back to a plain /contents/{path} read.
repo merge-upstream / sync-fork✅ (/merge-upstream)✅ (/sync_fork)Same feature, two paths — madtea hits both depending on platform.
Quota management⚙️/user/quota exists in Forgejo spec but is config-gated by [quota] ENABLED=true; 404 on default installs. Gitea has no /quota family.
ActivityPub federation⚙️/activitypub/... exists in Forgejo spec but is config-gated by [federation] ENABLED=true; 404 on default installs. Gitea has no /activitypub paths.
Repository flags⚙️/repos/{r}/flags exists in Forgejo spec but is config-gated by [repository] ENABLE_FLAGS=true; 404 on default installs.
Git notes API/repos/{r}/git/notes/{sha} is 404 on Forgejo default install — endpoint expected per spec, not present at runtime.

MCP tool annotations

Independent of the forge platform: every madt_* MCP tool declares all four MCP ToolAnnotations behaviour hints (readOnlyHint, destructiveHint, idempotentHint, openWorldHint), classified by each tool’s most dangerous action — see architecture/mcp-tool-annotations.md for the per-tool table. These are advisory hints: Claude Code currently surfaces no permission or UX effect from them, but other MCP clients may honour them (e.g. a confirmation prompt on destructive or open-world tools).

A second layer of annotations rides in each tool’s _meta map. Both keys below are Claude-Code-specific and ignored by every non-Anthropic client, so they carry no cross-platform risk. _meta["anthropic/maxResultSizeChars"] declares a per-tool output budget on the five large-output tools (madt_log, madt_prs, madt_files, madt_search, madt_issues); no Claude Code version floor is documented as of 2026-07, so treat it as current. _meta["anthropic/requiresUserInteraction"] forces a per-call permission prompt and is reserved for the one gated, wholly-dangerous tool — the instance-administration tool (madt_admin, whose surface is dominated by irreversible deletes); the flag is per-TOOL, so every action on it prompts (including madt_admin’s read-only listers) — it requires Claude Code v2.1.199+. (The irreversible forge-data deletes are omitted from every madtea surface entirely per ADR 0029, so there is no delete tool to flag.) Per-tool values and the reserve-list rationale live in internal/mcp/tool_meta_annotations.go.


Source specs: docs/api-specs/gitea-1.26.1.json, docs/api-specs/forgejo-15.0.json. We track only the latest stable of each platform. Older versions and pre-release dev branches are out of scope. Verified empirically against gitea:1.26.1 and codeberg.org/forgejo/forgejo:15.0.1+gitea-1.22.0 containers via tests/integration/forgejo-smoke.sh and the surface-diff in internal/gitea/api_surface_test.go.

Canonical source: docs/COMPAT.md in the madtea repo.