- Shell 73.3%
- Makefile 15%
- Dockerfile 11.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .agents/skills | ||
| .claude/skills | ||
| .forgejo/workflows | ||
| docs | ||
| scripts | ||
| .dockerignore | ||
| .mcp.json | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| Dockerfile | ||
| GLOSSARY.md | ||
| LICENSE | ||
| Makefile | ||
| pin.env | ||
| README.md | ||
| renovate.json | ||
| skills-lock.json | ||
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
commandandargswork in any MCP client that starts stdio servers; only Claude Code's.mcp.jsonis shown here. - Forgejo on your own machine. Inside the container,
localhostis the container, not the Consumer's host. PointFORGEJO_URLathost.docker.internalinstead (on Linux, also add--add-host=host.docker.internal:host-gatewayto theargs).
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.0is required: the server listens on loopback by default, which inside a container can't be reached through the published port.--allowed-hostsis required as soon as--hostisn't loopback: the server refuses to start without it. List the host names your MCP clients use in the URL; a request with any otherHostheader gets403 Forbidden. With the snippet above, uselocalhost, not127.0.0.1.-p 127.0.0.1:8080:8080publishes the port on this machine only.- The server gets no token. Every request must carry the caller's own token in an
Authorization: token …(orBearer …) header, which the server passes on to Forgejo; a request without one gets401 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_OFalready equalsUPSTREAM_VERSION, raiseREBUILDby one; - otherwise set
REBUILD_OFto the currentUPSTREAM_VERSIONandREBUILDto2.
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:
- checks the Pin against Upstream (
make verify-pin); - builds the Image for linux/amd64 and linux/arm64 from Upstream source;
- 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
mainthat 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.
REBUILDraised without updatingREBUILD_OF.