---
slug: "forgejo-mcp-server"
source_type: "readme"
source_url: "https://cdn.jsdelivr.net/gh/goern/forgejo-mcp@main/README.md"
repo: "https://github.com/goern/forgejo-mcp"
source_file: "README.md"
branch: "main"
---
# Forgejo MCP Server

Connect your AI assistant to Forgejo repositories. Manage issues, pull requests, files, and more through natural language.

## What It Does

Forgejo MCP Server is an integration plugin that connects Forgejo with [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) systems. Once configured, you can interact with your Forgejo repositories through any MCP-compatible AI assistant like Claude, Cursor, or VS Code extensions.

**Example commands you can use:**
- "List all my repositories"
- "Create an issue titled 'Bug in login page'"
- "Show me open pull requests in my-org/my-repo"
- "Get the contents of README.md from the main branch"
- "Show me the latest Actions workflow runs in goern/forgejo-mcp"

## Quick Start

### 1. Install

**Option A: Using Go (Recommended)**

```bash
git clone https://codeberg.org/goern/forgejo-mcp.git
cd forgejo-mcp
go install .
```

Ensure `$GOPATH/bin` (typically `~/go/bin`) is in your PATH.

> **Note:** `go install codeberg.org/goern/forgejo-mcp/v2@latest` does not work currently. See [Known Issues](#known-issues).

**Option B: Download Binary**

Download the latest release from the [releases page](https://codeberg.org/goern/forgejo-mcp/releases).

For Arch Linux, use your favorite AUR helper:

```bash
yay -S forgejo-mcp      # builds from source
yay -S forgejo-mcp-bin  # uses pre-built binary
```

**Option C: Nix / NixOS**

 You can run the server directly using the Nix package manager:
 ```bash
 nix-shell -p forgejo-mcp
 ```
 Or using Flakes:
 ```bash
 nix run nixpkgs#forgejo-mcp
 ```

 > **Note:** `forgejo-mcp` is currently only available in the `unstable` channel and is not yet part of the 25.11 stable release.

**Option D: Container image**

A signed multi-stage OCI image is published on every release to
`codeberg.org/goern/forgejo-mcp`. Run the server without building from source:

```bash
# Latest release
podman run --rm -i \
  -e FORGEJO_ACCESS_TOKEN="<your personal access token>" \
  codeberg.org/goern/forgejo-mcp:latest \
  --transport stdio --url https://your-forgejo-instance.org

# Or pin a specific version
podman run --rm -i codeberg.org/goern/forgejo-mcp:v2.24.0 --help
```

| Tag             | Meaning                                                         |
|-----------------|----------------------------------------------------------------|
| `vMAJOR.MINOR.PATCH` | Immutable — the exact release (e.g. `v2.24.0`). Use in production. |
| `latest`        | Moving — tracks the most recent release. Convenience only.     |

The image is single-arch (`linux/amd64`), signed with cosign, and carries an
attached CycloneDX SBOM. See [Verify the container image](#6-verify-the-container-image)
to check the signature and provenance before running.

### 2. Get Your Access Token

1. Log into your Forgejo instance
2. Go to **Settings** → **Applications** → **Access Tokens**
3. Create a new token with the permissions you need (repo, issue, etc.)

### 3. Configure Your AI Assistant

Add this to your MCP configuration file:

**For stdio mode** (most common):

```json
{
  "mcpServers": {
    "forgejo": {
      "command": "forgejo-mcp",
      "args": [
        "--transport", "stdio",
        "--url", "https://your-forgejo-instance.org"
      ],
      "env": {
        "FORGEJO_ACCESS_TOKEN": "<your personal access token>",
        "FORGEJO_USER_AGENT": "forgejo-mcp/1.0.0"
      }
    }
  }
}
```

**For streamable HTTP mode** (recommended for remote/Claude.ai):

```json
{
  "mcpServers": {
    "forgejo": {
      "url": "http://localhost:8080/mcp"
    }
  }
}
```

When using streamable HTTP mode, start the server first:

```bash
forgejo-mcp --transport http --url https://your-forgejo-instance.org --token <your-token>
```

**Multi-tenant HTTP mode** (optional):

You can run a single centralized `forgejo-mcp` instance and let each client provide its own token via the standard HTTP `Authorization` header. This enables serving multiple users or agents from one server.

1. Start the server (optionally without any global token):
   ```bash
   forgejo-mcp --transport http --url https://your-forgejo-instance.org
   ```
2. Clients include their specific token in each request:
   - `Authorization: token <token>` (Forgejo style)
   - `Authorization: Bearer <token>` (OAuth2/MCP style)
   - Note: The scheme (`token` or `Bearer`) is case-insensitive.

See [demos/multi-tenant-http.md](https://github.com/goern/forgejo-mcp/blob/HEAD/demos/multi-tenant-http.md) for a copy-pasteable walkthrough.

> Design rationale, token-resolution rules, and request-isolation guarantees: see the `stateless-http-auth` OpenSpec change (`openspec/changes/archive/`).

**For SSE mode** (legacy HTTP-based):

```json
{
  "mcpServers": {
    "forgejo": {
      "url": "http://localhost:8080/sse"
    }
  }
}
```

When using SSE mode, start the server first:

```bash
forgejo-mcp --transport sse --url https://your-forgejo-instance.org --token <your-token>
```

### 4. Start Using It

Open your MCP-compatible AI assistant and try:

```
List all my repositories
```

## Available Tools

| Tool | Description |
|------|-------------|
| **User** | |
| `get_my_user_info` | Get information about the authenticated user |
| `check_notifications` | Check and list user notifications |
| `get_notification_thread` | Get detailed info on a single notification thread |
| `mark_notification_read` | Mark a single notification thread as read |
| `mark_all_notifications_read` | Acknowledge all notifications |
| `list_repo_notifications` | Filter notifications scoped to a single repository |
| `mark_repo_notifications_read` | Mark all notifications in a specific repo as read |
| `search_users` | Search for users |
| **Repositories** | |
| `list_my_repos` | List all repositories you own |
| `create_repo` | Create a new repository |
| `fork_repo` | Fork a repository |
| `search_repos` | Search for repositories |
| **Branches** | |
| `list_branches` | List all branches in a repository |
| `create_branch` | Create a new branch |
| `delete_branch` | Delete a branch |
| **Branch Protection** | |
| `list_branch_protections` | List a repository's branch protection rules. Bounded by `page` (1-based) + `limit` (page size); the response echoes `page`/`limit` so callers can fetch the next page. |
| `get_branch_protection` | Get a single rule by `rule` name |
| `create_branch_protection` | Create a rule. Requires `branch_name`; `status_check_contexts` is a comma-separated list of required checks (e.g. `"ci/build,ci/test"`). |
| `edit_branch_protection` | Edit a rule by `rule` name. Only fields you pass change; omitted fields are left unchanged. |
| `delete_branch_protection` | Delete a rule by `rule` name |
| **Files** | |
| `get_file_content` | Get the content of a file. Optional `start_line`/`end_line` request a 1-indexed inclusive line range (clamps to file extent; ignored when `with_metadata=true`). |
| `create_file` | Create a new file |
| `update_file` | Update an existing file |
| `delete_file` | Delete a file |
| **Commits** | |
| `list_repo_commits` | List commits in a repository |
| **Issues** | |
| `list_repo_issues` | List issues in a repository (page/limit) |
| `get_issue_by_index` | Get a specific issue |
| `create_issue` | Create a new issue |
| `add_issue_labels` | Add labels to an issue (requires numeric label IDs) |
| `remove_issue_labels` | Remove labels from an issue (requires numeric label IDs) |
| `update_issue` | Update an existing issue (requires numeric milestone ID) |
| `issue_state_change` | Open or close an issue |
| `list_issue_dependencies` | List issues the given issue depends on. Bounded by `page` (1-based) + `limit` (page size); the response echoes `page`/`limit` so callers can fetch the next page. |
| `list_issue_dependents` | List issues that depend on the given issue. Bounded by `page` (1-based) + `limit` (page size); the response echoes `page`/`limit` so callers can fetch the next page. |
| `add_issue_dependency` | Make one issue depend on another |
| `remove_issue_dependency` | Remove a dependency from an issue |
| `list_repo_milestones` | List milestones with their IDs (use with `update_issue`) |
| `list_repo_labels` | List labels with their IDs. Merges org-level labels for org-owned repos (set `include_org_labels=false` to opt out). Each entry carries a `scope` field (`"repo"` or `"org"`). |
| `list_org_labels` | List organization-level labels with their IDs (use with `add_issue_labels`, `remove_issue_labels`). |
| `create_repo_label` | Create a repository label (`name`, `color` as 6-digit hex, optional `description`). Returns numeric `id` for immediate use in `add_issue_labels`. |
| `edit_repo_label` | Edit a repository label (PATCH — only supplied fields change: `name`, `color`, `description`). |
| `delete_repo_label` | Delete a repository label. Refuses by default when the label is in use (reports count); set `delete_mode=force` to override. |
| `get_repo_label` | Get a single repository label by numeric `id`. |
| `create_org_label` | Create an organization-level label. Same fields as `create_repo_label`. |
| `edit_org_label` | Edit an organization-level label (PATCH semantics). |
| `delete_org_label` | Delete an organization-level label. In-use guard counts across visible org repos (best-effort); `delete_mode=force` overrides. |
| `get_org_label` | Get a single organization-level label by numeric `id`. |
| **Comments** | |
| `list_issue_comments` | List comments on an issue or PR |
| `get_issue_comment` | Get a specific comment |
| `create_issue_comment` | Add a comment to an issue or PR |
| `edit_issue_comment` | Edit a comment |
| `delete_issue_comment` | Delete a comment |
| **Pull Requests** | |
| `list_repo_pull_requests` | List pull requests in a repository |
| `get_pull_request_by_index` | Get a specific pull request |
| `create_pull_request` | Create a new pull request |
| `update_pull_request` | Update an existing pull request |
| `list_pull_reviews` | List reviews for a pull request |
| `get_pull_review` | Get a specific pull request review |
| `list_pull_review_comments` | List comments on a pull request review |
| `list_pull_request_files` | List changed files in a pull request (paginated). Use the returned filenames as the `file_path` argument to `get_pull_request_diff`. |
| `get_pull_request_diff` | Get the unified diff of a pull request. Optional `file_path` returns only that file's hunks (matches on either pre- or post-rename path). |
| `merge_pull_request` | Merge a pull request (style: merge/rebase/rebase-merge/squash; optional title/message/delete-branch/force-merge/wait-for-checks). |
| `create_pull_review` | Create a review on a pull request (state: APPROVED/REQUEST_CHANGES/COMMENT) with optional inline comments. |
| **Actions** | |
| `dispatch_workflow` | Trigger a workflow run via `workflow_dispatch` event |
| `list_workflow_runs` | List workflow runs with optional filtering by status, event, or SHA |
| `get_workflow_run` | Get details of a specific workflow run by ID |
| `list_action_run_jobs` | List jobs for a Forgejo v16+ workflow run with client-side `page` and `limit` bounds |
| `get_action_job_logs` | Read a Forgejo v16+ job log with resumable `offset` and `max_bytes` bounds; defaults to the tail |
| **Organizations** | |
| `search_org_teams` | Search for teams in an organization |
| **Time Tracking** | |
| `list_issue_tracked_times` | List tracked time entries on an issue or PR |
| `list_repo_tracked_times` | List tracked time entries across a repository |
| `list_my_tracked_times` | List your own tracked time entries |
| `add_issue_time` | Log time against an issue or PR (accepts seconds or duration like `15m`) |
| `reset_issue_time` | Delete ALL tracked time entries on an issue or PR (destructive) |
| `delete_issue_time_entry` | Delete a single tracked time entry by ID |
| `start_issue_stopwatch` | Start a stopwatch on an issue or PR |
| `stop_issue_stopwatch` | Stop a running stopwatch and record the elapsed time |
| `cancel_issue_stopwatch` | Cancel a running stopwatch without recording |
| `list_my_stopwatches` | List currently running stopwatches |
| **Attachments** | |
| `list_issue_attachments` | List attachments on an issue or PR |
| `get_issue_attachment` | Get metadata for a single issue/PR attachment |
| `download_issue_attachment` | Download an issue/PR attachment (inline if < 1 MiB; metadata + URL otherwise) |
| `create_issue_attachment` | Upload a new attachment to an issue or PR (base64 content) |
| `edit_issue_attachment` | Rename an issue/PR attachment |
| `delete_issue_attachment` | Delete an issue/PR attachment |
| `list_comment_attachments` | List attachments on an issue/PR comment |
| `get_comment_attachment` | Get metadata for a single comment attachment |
| `download_comment_attachment` | Download a comment attachment (inline if < 1 MiB; metadata + URL otherwise) |
| `create_comment_attachment` | Upload a new attachment to an issue/PR comment (base64 content) |
| `edit_comment_attachment` | Rename a comment attachment |
| `delete_comment_attachment` | Delete a comment attachment |
| **Releases** | |
| `list_releases` | List releases for a repository (page/limit + client-side `state` filter: all/draft/prerelease/published) |
| `get_release_by_id` | Get a release by numeric ID |
| `get_release_by_tag` | Get a release by tag name |
| `get_latest_release` | Get the latest non-draft, non-prerelease release |
| `create_release` | Create a new release (pass `target_commitish` to also create the tag) |
| `edit_release` | Update fields of an existing release (only supplied fields are sent) |
| `delete_release` | Delete a release by numeric ID — destructive |
| `delete_release_by_tag` | Delete a release by tag name — destructive, verify tag |
| `list_release_attachments` | List attachments on a release (response fetched in full, sliced client-side) |
| `get_release_attachment` | Get metadata for a single release attachment |
| `download_release_attachment` | Download a release attachment (inline if < 1 MiB; metadata + URL otherwise) |
| `create_release_attachment` | Upload a new attachment to a release (base64 content) |
| `edit_release_attachment` | Rename a release attachment |
| `delete_release_attachment` | Delete a release attachment — destructive |
| **Wiki** | |
| `list_wiki_pages` | List pages using `page`/`limit`; returns `has_next`. |
| `get_wiki_page` | Read decoded Markdown; optional `start_line`/`end_line`, always returns `total_lines`. |
| `get_wiki_revisions` | List revision history using `page`/`limit`; returns `has_next`. |
| `create_wiki_page` | Create a page and return its server-normalized `page_name`; slash-separated titles are a flat subpage naming convention (no hierarchy and no automatic parent page), and an existing title is overwritten. |
| `update_wiki_page` | Update content/title by normalized `page_name`; last writer wins. |
| `delete_wiki_page` | Delete a page by normalized `page_name`. |
| **Server** | |
| `get_forgejo_mcp_server_version` | Get the MCP server version |

## Resources

MCP resource templates expose Forgejo entities as URI-addressable resources using the `forgejo://` scheme. The URI scheme is instance-portable — the same URI form works against any Forgejo instance — and does not collide with Forgejo web links. Clients that support `resources/templates/list` and `resources/read` (Claude Code, Claude Desktop, Codex, Cursor) can resolve these URIs directly. Clients without resource-template support continue to use the tools above — no functionality is removed.

Resources do NOT replace any MCP tool — every existing list/get tool stays available; resources are an additive, URI-addressable read surface intended for auto-resolution from LLM context and for content-addressable caching of immutable entities like commits.

Resources that embed a list (issue, pr) cap the embedded array at 30 items. When truncated, the JSON payload includes a sentinel naming the corresponding `list_*` tool the caller should invoke for the full list.

**When to use resources vs tools:** prefer a resource when you have a specific sha or index in hand; prefer a tool when listing or searching.

| URI Template | Entity | Notes |
|---|---|---|
| `forgejo://owner/{owner}` | application/json | User or org profile addressed by login; resolves user first, falls back to org. |
| `forgejo://repo/{owner}/{repo}` | application/json | Repository overview: identity + counts, no embedded lists. |
| `forgejo://repo/{owner}/{repo}/commit/{sha}` | Commit metadata | Immutable per sha. Returns JSON + markdown sidecar. sha must be 40 hex chars. |
| `forgejo://repo/{owner}/{repo}/commit/{sha}/status` | application/json | Combined CI status for a sha: aggregate state + bounded per-context statuses (cap 30, sentinel names list tool `get_commit_statuses`). |
| `forgejo://repo/{owner}/{repo}/issue/{index}` | application/json (+ text/markdown sidecar) | Issue metadata + rendered body + bounded recent comments (cap 30, sentinel names `list_issue_comments`). |
| `forgejo://repo/{owner}/{repo}/{kind}/{index}/comment/{id}` | application/json (+ text/markdown sidecar) | Single comment by id; kind ∈ {issue, pr}. |
| `forgejo://repo/{owner}/{repo}/pr/{index}` | application/json (+ text/markdown sidecar) | PR metadata, head/base refs, mergeability, bounded recent comments (cap 30, sentinel `list_issue_comments`) and reviews (cap 30, sentinel `list_pull_reviews`). |
| `forgejo://repo/{owner}/{repo}/label/{id}` | application/json | Single repository label by numeric id. |
| `forgejo://repo/{owner}/{repo}/labels{?page,limit}` | application/json | Bounded list of repository labels (cap 30, sentinel names `list_repo_labels`). |
| `forgejo://org/{org}/labels{?page,limit}` | application/json | Bounded list of organization-level labels (cap 30, sentinel names `list_org_labels`). |
| `forgejo://repo/{owner}/{repo}/wiki/{pageName}` | application/json (+ text/markdown sidecar) | Wiki page with bounded revisions and Markdown capped at 1 MiB. Use the returned normalized `page_name`; encode a literal `/` as `%2F` and spaces as `%20` in the URI (do not double-encode an already normalized name). |

Slash-separated titles such as `Guides/Setup` are useful as a subpage naming convention,
but Forgejo stores the pages in a flat list: it neither creates `Guides` automatically nor
records a parent-child relationship. Create the parent separately when readers need it, and
always address subsequent calls with the normalized `page_name` returned by create or list.
The wiki REST behavior documented here was live-tested against Forgejo
`15.0.4+gitea-1.22.0`; the complete MCP demo was replayed successfully against
`16.0.0+gitea-1.22.0`. This is the tested range, not a guarantee for every intermediate
or differently proxied deployment.

### Client Compatibility

| Client | `resources/templates/list` | `resources/read` |
|---|---|---|
| Claude Code | supported | supported |
| Claude Desktop | supported | supported |
| Codex | supported | supported |
| Cursor (current) | supported | supported |
| Older / minimal clients | tools only | tools only |

## Demos

End-to-end, copy-pasteable walkthroughs of the tools above — grouped by
topic (labels, attachments, time tracking, notifications, orgs, bounded
I/O code review, transport) — live in [demos/](https://github.com/goern/forgejo-mcp/blob/HEAD/demos/README.md). Each
demo pairs real `./forgejo-mcp --cli` invocations with the output they
produced against `codeberg.org`.

## CLI Mode

You can invoke any tool directly from the command line without running an MCP server. This is useful for shell scripts, CI/CD pipelines, and Claude Code skills.

```bash
# List all available tools (grouped by domain)
forgejo-mcp --cli list

# Invoke a tool with JSON arguments
forgejo-mcp --cli get_issue_by_index --args '{"owner":"goern","repo":"forgejo-mcp","index":1}'

# Pipe JSON arguments via stdin
echo '{"owner":"goern","repo":"forgejo-mcp"}' | forgejo-mcp --cli list_repo_issues

# List recent workflow runs (text output)
forgejo-mcp --cli list_workflow_runs \
  --args '{"owner":"goern","repo":"forgejo-mcp"}' \
  --output=text

# List only failed runs
forgejo-mcp --cli list_workflow_runs \
  --args '{"owner":"goern","repo":"forgejo-mcp","status":"failure"}' \
  --output=text

# List jobs and inspect the tail of a failed job (Forgejo v16+)
forgejo-mcp --cli list_action_run_jobs \
  --args '{"owner":"goern","repo":"forgejo-mcp","run_id":123}' \
  --output=text
forgejo-mcp --cli get_action_job_logs \
  --args '{"owner":"goern","repo":"forgejo-mcp","job_id":456,"max_bytes":32768}' \
  --output=text

# Forgejo's run-wide ZIP log endpoint has no Range support. Enumerate jobs and
# fetch their bounded plaintext logs instead.

# Show a tool's parameters
forgejo-mcp --cli create_issue --help

# Control output format (json or text)
forgejo-mcp --cli list --output=json
forgejo-mcp --cli get_my_user_info --args '{}' --output=text
```

CLI mode requires the same `FORGEJO_URL` and `FORGEJO_ACCESS_TOKEN` configuration as MCP server mode. Tool results are written as JSON to stdout by default; errors go to stderr with a non-zero exit code.

## Configuration Options

You can configure the server using command-line arguments or environment variables:

| CLI Argument | Environment Variable | Description |
|--------------|---------------------|-------------|
| `--url` | `FORGEJO_URL` | Your Forgejo instance URL |
| `--token` | `FORGEJO_ACCESS_TOKEN` | Your personal access token |
| `--debug` | `FORGEJO_DEBUG` | Enable debug mode |
| `--transport` | - | Transport mode: `stdio`, `sse`, or `http` |
| `--sse-port` | - | Port for SSE mode (default: 8080) |
| `--http-port` | - | Port for streamable HTTP mode (default: 8080) |
| `--cli` | - | Enter CLI mode for direct tool invocation |
| `--user-agent` | `FORGEJO_USER_AGENT` | HTTP User-Agent header (default: `forgejo-mcp/<version>`) |

Command-line arguments take priority over environment variables.

## Verifying Releases

Release archives are accompanied by a `checksums.txt` file and an optional
`checksums.txt.sig` produced by [cosign](https://github.com/sigstore/cosign)
with the project's release keypair. Verifying both files lets you confirm
that the binary you downloaded was built by the project's release pipeline
and has not been tampered with in transit.

> **Heads up:** cosign signing was introduced mid-2026. Tags released
> before signing was wired up ship without a `.sig` file — verification
> applies from `v2.23.x` onward only, and only when the
> `COSIGN_PRIVATE_KEY` secret was configured at release time.

### 1. Install cosign

Follow the upstream
[cosign installation guide](https://docs.sigstore.dev/cosign/system_config/installation/)
for your platform. Quick paths:

```bash
# Linux/macOS — pinned binary
COSIGN_VERSION=v2.4.1
curl -sSfL -o /usr/local/bin/cosign \
  "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64"
chmod +x /usr/local/bin/cosign

# macOS via Homebrew
brew install cosign

# Arch Linux
sudo pacman -S cosign
```

Confirm:

```bash
cosign version
```

### 2. Fetch the public key

The normative source for the cosign public key is the
[`op1st-emea-b4mad`](https://codeberg.org/operate-first/op1st-emea-b4mad)
GitOps repo — same source of truth that provisions the
`cosign-signing-key-artifacts` Secret in the `op1st-pipelines` namespace
where the release pipeline runs. This is the **artifact-signing** key that
signs release blobs (`checksums.txt.sig`); it is distinct from the
**image-signing** key `cosign-signing-key-images.pub` used in §5–§6 below.
Two ways to fetch it:

**Branch-tip (live, follows future key rotations):**

```bash
curl -sSfL -o cosign.pub \
  https://codeberg.org/operate-first/op1st-emea-b4mad/raw/branch/main/manifests/applications/op1st-pipelines-tokens/cosign-signing-key-artifacts.pub
```

**Commit-pinned (tamper-evident, recommended for CI/scripts):**

```bash
curl -sSfL -o cosign.pub \
  https://codeberg.org/operate-first/op1st-emea-b4mad/raw/commit/cd3715fa8283a2069a2e3e299744a7b55b1b0260/manifests/applications/op1st-pipelines-tokens/cosign-signing-key-artifacts.pub
```

The commit-pinned permalink hashes its content into the URL — if anyone
ever rewrites the file at that commit, your download fails or mismatches.
Pin to the latest commit that you trust before adopting the key in
automation.

### 3. Download the release artifacts

Pick the tag you installed (e.g. `v2.23.1`) and grab the checksum file,
its signature, and the binary archive:

```bash
TAG=v2.23.1
VERSION="${TAG#v}"
BASE="https://codeberg.org/goern/forgejo-mcp/releases/download/${TAG}"

curl -sSfLO "${BASE}/forgejo-mcp_${VERSION}_checksums.txt"
curl -sSfLO "${BASE}/forgejo-mcp_${VERSION}_checksums.txt.sig"
curl -sSfLO "${BASE}/forgejo-mcp_${VERSION}_linux_amd64.tar.gz"   # adjust os/arch
```

### 4. Verify the signature, then the checksum

Cosign verifies that `checksums.txt` was signed by the holder of the
private key matching `cosign.pub`. Once the checksum file is trusted, a
plain `sha256sum -c` check confirms the archive's integrity.

```bash
# Verify checksums.txt against the signature.
cosign verify-blob \
  --key cosign.pub \
  --signature "forgejo-mcp_${VERSION}_checksums.txt.sig" \
  "forgejo-mcp_${VERSION}_checksums.txt"
# Expected: "Verified OK"

# Verify the downloaded archive against the (now-trusted) checksums.
sha256sum --ignore-missing -c "forgejo-mcp_${VERSION}_checksums.txt"
# Expected: "<archive>: OK"
```

The checksum chain transitively covers the SBOMs and other per-archive
assets — verifying `checksums.txt` once is sufficient for everything
listed inside it.

### 5. Verify SLSA provenance for the release-tools image

The release-tools container image (used internally by the Tekton release
pipeline) carries SLSA v1.0 provenance generated by [Tekton
Chains](https://tekton.dev/docs/chains/). This attestation binds the image
digest to the exact PipelineRun, git commit, and builder identity that
produced it — providing supply-chain provenance beyond what the cosign
signature alone can attest.

Fetch the `cosign-signing-key-images` public key (a separate key from the
artifact-signing key above):

```bash
curl -sSfL -o cosign-images.pub \
  https://codeberg.org/operate-first/op1st-emea-b4mad/raw/branch/main/manifests/applications/op1st-pipelines-tokens/cosign-signing-key-images.pub
```

Verify the attestation against a specific image tag:

```bash
IMAGE_TAG=v1.0.0   # substitute the release-tools tag you want to verify
cosign verify-attestation \
  --type slsaprovenance \
  --key cosign-images.pub \
  "codeberg.org/operate-first/release-tools:${IMAGE_TAG}" \
  | jq .
```

A successful run prints the decoded in-toto statement (JSON). Check that
`predicate.buildDefinition.externalParameters.runSpec.params` references the
expected git revision, and `predicate.runDetails.builder.id` shows the
Tekton Chains builder.

> **Note:** SLSA provenance attestations are available from releases built
> after forgejo-mcp-46j (Tekton Chains support) landed. Earlier image tags
> carry only the cosign signature; they have no `verify-attestation` payload.

### 6. Verify the container image

The `codeberg.org/goern/forgejo-mcp` application image ([Option D](#1-install))
is signed by the same `cosign-signing-key-images` key as the release-tools
image, carries an attached CycloneDX SBOM, and gets SLSA v1.0 provenance from
Tekton Chains. Reuse the `cosign-images.pub` key fetched above.

Verify the signature:

```bash
IMAGE_TAG=v2.24.0   # substitute the release you are pulling
cosign verify \
  --key cosign-images.pub \
  "codeberg.org/goern/forgejo-mcp:${IMAGE_TAG}" \
  | jq .
```

Verify the SLSA provenance attestation:

```bash
cosign verify-attestation \
  --type slsaprovenance \
  --key cosign-images.pub \
  "codeberg.org/goern/forgejo-mcp:${IMAGE_TAG}" \
  | jq .
```

Verify and download the signed CycloneDX SBOM attestation:

```bash
cosign verify-attestation \
  --type cyclonedx \
  --key cosign-images.pub \
  "codeberg.org/goern/forgejo-mcp:${IMAGE_TAG}" \
  | jq -r '.payload | @base64d | fromjson | .predicate' > forgejo-mcp.cdx.json
```

> The SBOM is now a **signed** in-toto attestation (`cosign attest`), not an
> unsigned `attach sbom` artifact. `cosign download sbom` no longer applies.

Because the publish pipeline pushes by digest and only promotes the
`vX.Y.Z` / `latest` tags *after* signing and SBOM attachment succeed, any tag
you can pull is guaranteed to be signed.

### Troubleshooting verification

- **`Error: no matching signatures`** — the `.sig` file is from a
  different release, or `cosign.pub` is the wrong key. Re-download both
  from the same tag.
- **`Error: cannot read file: checksums.txt.sig`** — release predates
  cosign signing, or signing was skipped that run because the secret was
  unset. Fall back to the checksum-only check (`sha256sum -c`), which
  still detects in-transit corruption but not tampering.
- **Mismatch between `cosign.pub` and the signature** — confirm you
  fetched the public key from a commit that includes the key in use at
  the time of the release. If in doubt, fetch from `branch/main`.

## Troubleshooting

**Enable debug mode** to see detailed logs:

```bash
forgejo-mcp --transport sse --url <url> --token <token> --debug
```

Or set the environment variable:

```bash
export FORGEJO_DEBUG=true
```

**Custom User-Agent**: If your Forgejo instance or proxy blocks the default `go-http-client` user agent, set a custom one:

```bash
# Via environment variable
export FORGEJO_USER_AGENT="forgejo-mcp/1.0.0"

# Or via CLI flag
forgejo-mcp --user-agent "forgejo-mcp/1.0.0" --transport sse --url <url> --token <token>
```

## Getting Help

- [Report issues](https://codeberg.org/goern/forgejo-mcp/issues)
- [View source code](https://codeberg.org/goern/forgejo-mcp)

This repository is also mirrored on [Radicle](https://radicle.xyz/) — a peer-to-peer code collaboration network. Clone via:

```bash
rad clone rad:z4PdPpsH9iJQcWfqTbxpFcWaZ9zPL
```

## For Developers

See [DEVELOPER.md](https://github.com/goern/forgejo-mcp/blob/HEAD/DEVELOPER.md) for build instructions, architecture overview, and contribution guidelines.

## Known Issues

- **`go install ...@latest` fails** — The `go.mod` contains a `replace` directive (for a forked Forgejo SDK), which prevents remote `go install`. Use the clone-and-build workflow shown in [Quick Start](#quick-start) instead. Tracked in [#67](https://codeberg.org/goern/forgejo-mcp/issues/67).

## Contributors

forgejo-mcp is shaped by everyone who files issues, writes code, reviews PRs, and pushes the project forward. Thank you all. 🙏

### Code contributors

| Contributor | Highlights |
|-------------|------------|
| [goern](https://codeberg.org/goern) (Christoph Görn) | Project creator and maintainer |
| Ronmi Ren | Co-creator; SSE/HTTP transport, issue blocking, CI/CD improvements, logo, Glama spec |
| [twstagg](https://codeberg.org/twstagg) (Tristin Stagg) | User agent configuration support (PR #89) |
| [mattdm](https://codeberg.org/mattdm) (Matthew Miller) | Logging improvements, FORGEJO_* migration, README, URL refactor |
| [byteflavour](https://codeberg.org/byteflavour) | `check_notifications` + full notification management API (PR #84, #86); stateless per-request auth for HTTP/SSE transports (PR #138); NixOS installation docs (PR #146); feature requests #80, #85 |
| [jesterret](https://codeberg.org/jesterret) | Pull request reviews and comments support (PR #51) |
| [appleboy](https://codeberg.org/appleboy) | Custom SSE port support, bug fixes |
| [ignasgil](https://codeberg.org/ignasgil) | `remove_issue_labels` tool (PR #96) |
| [dmikushin](https://codeberg.org/dmikushin) (Dmitry Mikushin) | Fix string-encoded number parameter parsing from MCP clients (PR #93) |
| [jiriks74](https://codeberg.org/jiriks74) | mcp-go v0.44.0 dependency update (PR #90) |
| [th](https://codeberg.org/th) (Tomi Haapaniemi) | `update_pull_request` tool |
| [hiifong](https://codeberg.org/hiifong) | Early bug fixes and updates |
| [Lunny Xiao](https://codeberg.org/lunny) | Early contributions |
| [techknowlogick](https://codeberg.org/techknowlogick) | Early contributions |
| [yp05327](https://codeberg.org/yp05327) | Early contributions |
| [mw75](https://codeberg.org/mw75) | Owner/org support for repo creation (PR #18) |
| [Dax Kelson](https://codeberg.org/dkelson) | Issue comment management (PR #34) |
| [Guruprasad Kulkarni](https://codeberg.org/comdotlinux) | Arch Linux AUR installation docs (PR #69) |
| [Mario Wolff](https://codeberg.org/mariowolff) | Contributions |
| [Massimo Fraschetti](https://codeberg.org/fraschetti) | Contributions |
| [synath](https://codeberg.org/synath) (David Paul Turley) | Repository-scoped token support via `ServerVersion` probe (PR #112); merge status-code check (PR #113); Claude Desktop Extension (.mcpb) packaging (PR #118) |
| [BrilliantKahn](https://codeberg.org/BrilliantKahn) | `get_file_content` plain-text default (PR #116); `list_repo_contents` and `get_repo_tree` tools (PR #117). **First-ever open source contribution** — welcome aboard! 🎉 |

### Community contributors

Issue reporters and discussion participants who shaped the direction of the project:

| Contributor | Contributions |
|-------------|--------------|
| [byteflavour](https://codeberg.org/byteflavour) | Filed #80 (milestone/label discovery), #85 (notification API proposal); active reviewer in discussions |
| [choucavalier](https://codeberg.org/choucavalier) | Filed #82 (fix skill), #70 (macOS arm64 releases), #62 (binary releases & mise support) |
| [MalcolmMielle](https://codeberg.org/MalcolmMielle) | Filed #59 (PR review tools — since implemented) |
| [redbeard](https://codeberg.org/redbeard) | Filed #60 (Actions support — since implemented) |
| [c6sepl6p](https://codeberg.org/c6sepl6p) | Filed #72 (base64 encoding), #54 (merge pull request — since implemented) |
| [malik](https://codeberg.org/malik) | Filed #73 (version flag), #47 (Nix build fix) |
| [a2800276](https://codeberg.org/a2800276) | Filed #74 (OpenAI compatibility) |
| [simenandre](https://codeberg.org/simenandre) | Filed #49 (go install support) |
| [BasdP](https://codeberg.org/BasdP) | Filed #42 (Projects support) |
| [BoBeR182](https://codeberg.org/BoBeR182) | Filed #32 (wiki support) |
| [ignasgil](https://codeberg.org/ignasgil) | Filed #95 (`remove_issue_labels` feature request) |
| [Vokuar](https://codeberg.org/Vokuar) | Filed #99 (streamable HTTP transport support) |
| [janbaer](https://codeberg.org/janbaer) | Filed #98 (reply to review comment) |
| [fraschm98](https://codeberg.org/fraschm98) | Early issue reports |
| [heathen711](https://codeberg.org/heathen711) | Filed #106 (issue/comment attachments — since implemented); shaped the 1 MiB inline cap + `browser_download_url` fall-through design |

### Cyborg contributors

This project also received contributions from AI coding agents — submitted as regular PRs, reviewed by humans:

| Agent | Role | Contributions |
|-------|------|---------------|
| [brenner-axiom](https://codeberg.org/brenner-axiom) (b4-dev, B4arena) | AI dev agent | Organization management tools (PR #94); showboat demos (PR #97); `list_repo_milestones`, `list_repo_labels` tools (PR #83); race condition fix (PR #78); contributors docs (PR #87, #88); filed #76; code reviews |
| opencode | AI dev agent | Pull request reviews and comments support (PR #51) |
| claude-code | AI dev agent | `get_file_content` plain-text default and `list_repo_contents`/`get_repo_tree` tools, paired with [BrilliantKahn](https://codeberg.org/BrilliantKahn) (PR #116, #117) |
| b4mad-release-bot | Release automation | Automated changelog and release tagging |
| the #B4mad Renovate bot | Dependency updates | Automated dependency upgrades |

Want to contribute? Open an issue or pull request — all are welcome.


## License

This project is open source. See the repository for license details.
