Multi-arch container image of the Forgejo MCP server, built from a pinned upstream release. https://code.vicoli.de/vicoli-oss/-/packages/container/forgejo-mcp
  • Shell 73.3%
  • Makefile 15%
  • Dockerfile 11.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-30 18:56:09 +00:00
.agents/skills chore(skills): add commit-push-pr and conventional-commit skills 2026-09-30 11:51:12 +00:00
.claude/skills chore(skills): add commit-push-pr and conventional-commit skills 2026-09-30 11:51:12 +00:00
.forgejo/workflows ci: skip an existing X.Y.Z-rN built from the same Pin, whichever run comes first (#7) 2026-09-30 19:17:51 +02:00
docs docs(agents): point issue tracker at vicoli-oss/docker-forgejo-mcp 2026-09-30 16:16:59 +02:00
scripts ci: skip an existing X.Y.Z-rN built from the same Pin, whichever run comes first (#7) 2026-09-30 19:17:51 +02:00
.dockerignore feat(image): build multi-arch Image from the Pin 2026-09-30 15:26:44 +02:00
.mcp.json feat: dogfood the Image as this repo's Forgejo MCP server (#31) 2026-09-30 20:02:59 +02:00
AGENTS.md docs(agents): configure issue tracker, triage labels and domain docs 2026-09-30 11:51:12 +00:00
CLAUDE.md docs(agents): point issue tracker at vicoli-oss/docker-forgejo-mcp 2026-09-30 16:16:59 +02:00
Dockerfile ci: publish the Image on push to main and dispatch (#7) 2026-09-30 17:50:10 +02:00
GLOSSARY.md docs: publish the Image from a public vicoli-oss org 2026-09-30 15:47:21 +02:00
LICENSE chore: add MIT license for this repo's own files 2026-09-30 16:31:53 +02:00
Makefile ci: publish the Image on push to main and dispatch (#7) 2026-09-30 17:50:10 +02:00
pin.env chore(deps): update upstream forgejo-mcp to v3.2.0 2026-09-30 17:05:02 +00:00
README.md docs: adopt the Image via .mcp.json with --pull=always (#32, #35) 2026-09-30 20:54:08 +02:00
renovate.json chore(renovate): no Dependency Dashboard issue in the triage tracker 2026-09-30 17:29:00 +02:00
skills-lock.json chore(skills): add commit-push-pr and conventional-commit skills 2026-09-30 11:51:12 +00:00

docker-forgejo-mcp

A multi-arch (linux/amd64, linux/arm64) Image of the Forgejo MCP server, for running it as an MCP server in your coding projects:

code.vicoli.de/vicoli-oss/forgejo-mcp

The Image is public: pulling it needs no docker login.

What this is

Upstream publishes its own image, but only for linux/amd64. This repo builds the Image from Upstream source instead, so it runs natively on arm64 Macs too (ADR 0001). The server itself is unchanged: the Image contains Upstream's binary, built from Upstream's Git repository at the commit recorded in the Pin, on a minimal, non-root, static base image. No Forgejo URL or token is baked in.

Upstream is often linked as codeberg.org/goern/forgejo-mcp. That repository is now a read-only mirror; Upstream development and releases happen at git.b4mad.industries/agentic-forges/forgejo-mcp, and this repo builds from there (ADR 0002).

For what the server can do (its tools, flags and environment variables), see Upstream's README.

Licenses. This repo (the Dockerfile, Makefile, scripts and CI) is MIT. The Image contains Upstream's software, which is licensed under GPL-3.0; its license text is in the Image at /usr/share/licenses/forgejo-mcp/LICENSE. The corresponding source is Upstream at the commit in the Pin, which the Image also records in its de.vicoli.upstream.source and de.vicoli.upstream.revision labels.

Quick start

The server needs your Forgejo instance's URL and a Forgejo access token (Settings → Applications → Access Tokens on your Forgejo, with the permissions you want the agent to have). Both come from your environment, never from a file you commit:

export FORGEJO_URL=https://forgejo.example.org

Set FORGEJO_ACCESS_TOKEN the way you keep other secrets, e.g. from a password manager's CLI or a shell profile that isn't in any repository.

Run the server over stdio. -e NAME without a value passes the variable through from your environment, so the token never appears on the command line:

docker run -i --rm -e FORGEJO_URL -e FORGEJO_ACCESS_TOKEN code.vicoli.de/vicoli-oss/forgejo-mcp:3

To check that it works, send it an MCP initialize request:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"check","version":"0"}}}' \
  | docker run -i --rm -e FORGEJO_URL -e FORGEJO_ACCESS_TOKEN code.vicoli.de/vicoli-oss/forgejo-mcp:3

The server logs to stderr that it connected to your Forgejo, and answers on stdout with {"jsonrpc":"2.0","id":1,"result":{…,"serverInfo":{"name":"Forgejo MCP Server","version":"3.2.0"}}}.

Claude Code setup

You need Docker and a Forgejo personal access token (see Quick start).

Add the server to your project's .mcp.json. Claude Code starts the Image over stdio and passes FORGEJO_URL and FORGEJO_ACCESS_TOKEN through from the environment you start claude in, so the file holds no token and is safe to commit:

{
  "mcpServers": {
    "forgejo": {
      "type": "stdio",
      "command": "docker",
      "args": [
        "run", "-i", "--rm", "--pull=always",
        "-e", "FORGEJO_URL",
        "-e", "FORGEJO_ACCESS_TOKEN",
        "code.vicoli.de/vicoli-oss/forgejo-mcp:3"
      ]
    }
  }
}

:3 plus --pull=always means each session start picks up new minor and patch releases and Rebuilds; without the flag, Docker keeps using whichever :3 it pulled first.

Or write the same entry with the CLI:

claude mcp add --scope project forgejo -- \
  docker run -i --rm --pull=always -e FORGEJO_URL -e FORGEJO_ACCESS_TOKEN code.vicoli.de/vicoli-oss/forgejo-mcp:3

Drop --scope project to keep the server to yourself instead of sharing it through .mcp.json. Either way, export both FORGEJO_URL and FORGEJO_ACCESS_TOKEN before starting claude. If you already keep your token under another name, add e.g. export FORGEJO_ACCESS_TOKEN=$FORGEJO_TOKEN to your shell profile. Claude Code asks you once to approve a server from a project's .mcp.json; after that, claude mcp list shows forgejo as connected.

  • Token scopes are the only guardrail. Upstream has no read-only mode or tool filtering: the agent can do whatever the token allows, so grant the smallest scopes that cover your use.
  • Other MCP clients. The same command and args work in any MCP client that starts stdio servers; only Claude Code's .mcp.json is shown here.
  • Forgejo on your own machine. Inside the container, localhost is the container, not the Consumer's host. Point FORGEJO_URL at host.docker.internal instead (on Linux, also add --add-host=host.docker.internal:host-gateway to the args).

HTTP mode

Instead of Claude Code starting one container per session over stdio, you can run one long-lived server with Upstream's streamable HTTP transport, by overriding the Image's CMD (--transport stdio):

docker run -d --name forgejo-mcp -p 127.0.0.1:8080:8080 -e FORGEJO_URL \
  code.vicoli.de/vicoli-oss/forgejo-mcp:3 \
  --transport http --host 0.0.0.0 --allowed-hosts localhost
  • --host 0.0.0.0 is required: the server listens on loopback by default, which inside a container can't be reached through the published port.
  • --allowed-hosts is required as soon as --host isn't loopback: the server refuses to start without it. List the host names your MCP clients use in the URL; a request with any other Host header gets 403 Forbidden. With the snippet above, use localhost, not 127.0.0.1.
  • -p 127.0.0.1:8080:8080 publishes the port on this machine only.
  • The server gets no token. Every request must carry the caller's own token in an Authorization: token … (or Bearer …) header, which the server passes on to Forgejo; a request without one gets 401 Unauthorized.

The MCP endpoint is http://localhost:8080/mcp. In .mcp.json, Claude Code fills in ${FORGEJO_ACCESS_TOKEN} from its environment when it connects, so the file still holds no token:

{
  "mcpServers": {
    "forgejo": {
      "type": "http",
      "url": "http://localhost:8080/mcp",
      "headers": {
        "Authorization": "token ${FORGEJO_ACCESS_TOKEN}"
      }
    }
  }
}

Or with the CLI. Keep the single quotes, so your shell writes ${FORGEJO_ACCESS_TOKEN} into the config rather than the token itself:

claude mcp add --scope project --transport http forgejo http://localhost:8080/mcp \
  --header 'Authorization: token ${FORGEJO_ACCESS_TOKEN}'

Stop the server with docker rm -f forgejo-mcp. For serving remote MCP clients, SSE and the OAuth resource-server mode, see Upstream's README.

Tags

Tags follow the Upstream Version without the v, plus a Rebuild number (ADR 0003):

Tag Points to Moves?
3.2.0-r1, 3.2.0-r2, … exactly one Rebuild of one Upstream Version never
3.2.0 the newest Rebuild of Upstream Version v3.2.0 yes
3.2 the newest Rebuild of the newest 3.2.x yes
3 the newest Rebuild of the newest 3.x.y yes
latest the newest Rebuild of the newest Upstream Version yes

Which to use. :3 follows new Upstream Versions and Rebuilds within Upstream's major version 3, and never jumps to a new major version with breaking changes; it's what the snippets above use. Use :3.2.0-rN when you want the exact same Image every time, e.g. in CI, and raise it yourself.

Rebuilds. A Rebuild is a new Image of an Upstream Version that was already published, e.g. after a change to the Dockerfile or the base image. It gets the next -rN tag and the moving tags follow it; the Upstream code in it is the same. A fix to the packaging never pretends to be a new Upstream Version.

How bumps arrive. Renovate watches Upstream's tags and opens a pull request here that bumps the Pin to each new Upstream Version, linking Upstream's release notes. Once a maintainer merges it, CI publishes the new Image and moves the tags. Consumers on :3 get it on their next pull (docker pull code.vicoli.de/vicoli-oss/forgejo-mcp:3); Consumers on :3.2.0-rN stay where they are until they raise the tag.

For maintainers

The Pin

pin.env is the Pin: the single source of what the Image is built from. The Makefile and CI both read it.

Variable Meaning
UPSTREAM_VERSION the Upstream Version to build, e.g. v3.2.0
UPSTREAM_COMMIT the commit that Upstream's tag for it points to; the build fails if it doesn't match
REBUILD the Rebuild number, counted against REBUILD_OF
REBUILD_OF the Upstream Version REBUILD counts against

If REBUILD_OF isn't UPSTREAM_VERSION, the Rebuild is 1, whatever REBUILD says. That's why a bump to a new Upstream Version only changes UPSTREAM_VERSION and UPSTREAM_COMMIT, which is exactly what Renovate's pull requests do. make print-tag prints the Image tag the Pin resolves to.

Raising the Pin for a Rebuild. After a change to the Dockerfile or base image that should reach Consumers, raise the Rebuild in the same pull request:

  • if REBUILD_OF already equals UPSTREAM_VERSION, raise REBUILD by one;
  • otherwise set REBUILD_OF to the current UPSTREAM_VERSION and REBUILD to 2.

Check with make print-tag that it prints the tag you expect, e.g. 3.2.0-r2.

Building and smoke-testing locally

Needs Docker with buildx, make and jq. make help lists all targets.

make verify-pin   # check on Upstream that the Upstream Version's tag still points at the Pin's commit
make build        # build forgejo-mcp:<tag>-amd64 and forgejo-mcp:<tag>-arm64
make smoke-test   # MCP handshake + tools/list on each, against a stub Forgejo

The smoke test of the architecture your machine isn't runs under emulation; pass PLATFORMS=linux/arm64 (or linux/amd64) to both targets to build and test only one. make build-multiarch and make smoke-test-multiarch build and test one multi-platform Image with SBOM and provenance, as CI does; they need Docker's containerd image store. The smoke test (scripts/smoke-test.sh) contacts nothing outside your machine.

How a release happens

There is no manual release step: merging a pull request that changes the Pin into main is the release. CI (.forgejo/workflows/ci.yml) runs on every pull request and push to main:

  1. checks the Pin against Upstream (make verify-pin);
  2. builds the Image for linux/amd64 and linux/arm64 from Upstream source;
  3. smoke-tests both architectures.

On a push to main (or a manual dispatch from main), it then publishes that same tested Image with scripts/publish.sh: it pushes the immutable X.Y.Z-rN tag, then moves X.Y.Z, X.Y, X and latest to it wherever it's the newest Rebuild. Pull requests never publish. X.Y.Z-rN is never overwritten:

  • a push to main that doesn't change the Pin finds its tag already published and publishes nothing;
  • a push that changes the Pin but resolves to a tag that already exists fails, e.g. REBUILD raised without updating REBUILD_OF.