Wraps the official gitea-mcp server (binary copied from docker.gitea.com/gitea-mcp-server:1.7.0) and serves it two ways from one container: - :8000 mcpo (OpenAPI) for Open WebUI, using the container's service token and guarded by MCPO_API_KEY. - :8080 native streamable-HTTP MCP at /mcp for coding agents, with the service token stripped so each client must send its own Gitea token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.5 KiB
gitea-mcpo-docker
A single Docker image that exposes the official Gitea MCP server in two ways at once:
| Port | Protocol | For | Gitea identity |
|---|---|---|---|
8000 |
OpenAPI, via mcpo |
Open WebUI | The container's service token (GITEA_ACCESS_TOKEN) |
8080 |
Native MCP (streamable HTTP) at /mcp |
Coding agents (Claude Code etc.) | Each client's own token, sent as Authorization: Bearer <token> |
Both are served by the same gitea-mcp binary: mcpo spawns it as a stdio child,
and a second instance runs in gitea-mcp's own HTTP mode.
Build
docker build -t gitea-mcpo-docker .
Run
docker run -p 8000:8000 -p 8080:8080 \
-e GITEA_HOST=https://code.aneur.in \
-e GITEA_ACCESS_TOKEN=<service-account token> \
-e MCPO_API_KEY=some-secret \
gitea-mcpo-docker
Open WebUI
Add a tool server pointing at http://<host>:8000, with bearer auth set to
MCPO_API_KEY. Interactive API docs are at http://<host>:8000/docs (and
/openapi.json). These are served without authentication even when MCPO_API_KEY
is set; only the tool endpoints themselves require it.
Claude Code (or any MCP client)
claude mcp add --transport http gitea http://<host>:8080/mcp \
--header "Authorization: Bearer <your Gitea token>"
The HTTP endpoint never falls back to the service token: GITEA_ACCESS_TOKEN and
GITEA_ACCESS_TOKEN_FILE are stripped from that process's environment. A request
without a token gets a token is required tool error, and a request with one acts
as that token's user. Commits, comments and PRs are therefore attributed to whichever
account the client's token belongs to.
Environment variables
| Variable | Effect |
|---|---|
GITEA_HOST |
Required. Base URL of the Gitea instance, e.g. https://code.aneur.in. |
GITEA_ACCESS_TOKEN |
Token that the mcpo side (Open WebUI) uses. Not visible to the :8080 MCP endpoint. |
GITEA_ACCESS_TOKEN_FILE |
Alternative to the above: path to a file holding the token (e.g. a Docker secret). |
MCPO_API_KEY |
If set, mcpo tool endpoints require Authorization: Bearer <value>. If unset, mcpo serves unauthenticated. |
GITEA_READONLY |
true exposes only read-only tools, on both ports. |
GITEA_SCOPES / GITEA_TOOLS |
Comma-separated allowlist of tool scopes / tool names, on both ports (see gitea-mcp --help). |
GITEA_INSECURE |
true ignores TLS certificate errors talking to Gitea. |
GITEA_DEBUG |
true enables gitea-mcp debug logging. |
MCPO_PORT |
Port mcpo listens on. Default 8000. |
MCP_PORT |
Port the native MCP endpoint listens on. Default 8080. |
ENABLE_MCPO / ENABLE_MCP |
Set either to false to run only the other side. Both default true. |
MCP_MODE is ignored: gitea-mcp would otherwise let it override the transport,
so the entrypoint unsets it.
Process supervision
entrypoint.sh runs both servers under tini. If either one exits, the other is
stopped and the container exits non-zero, so the orchestrator restarts the whole
thing rather than leaving it half up. docker stop shuts both down promptly.
Known characteristics
- No TLS termination. This image serves plain HTTP; put a reverse proxy in front of it if you need TLS or network restriction.
- The
:8080endpoint has no auth of its own. It relies entirely on Gitea tokens: anyone who can reach it and holds a valid Gitea token can use it as that user, and no more than that user could do directly against the Gitea API. gitea-mcp's HTTP endpoint also serves/healthz(unauthenticated).
Pinned revisions
gitea-mcp: pinned viaARG GITEA_MCP_VERSIONin theDockerfile. The binary is copied from the official imagedocker.gitea.com/gitea-mcp-server(tags have novprefix). When bumping it, checkgitea-mcp --helpstill accepts whatentrypoint.shuses.mcpo: pinned viaARG MCPO_VERSION, with themcpSDK pinned alongside it viaARG MCP_SDK_VERSION(see the comment in theDockerfilefor why).
CI
.gitea/workflows/docker-build.yml builds and pushes
code.aneur.in/cloud/gitea-mcpo-docker:latest on every push to main, first
re-tagging the existing :latest as :previous. It logs in to the registry as the
triggering user, with the repo secret BUILD_API_TOKEN as the password. The default
Gitea Actions token can't write to the package registry on this instance, so that
secret must be a personal access token with package-write scope.