Files
homelab-mcp-gitea/README.md
T
homelab-mcp 0212913831 feat: initial homelab-mcp-gitea v0.1
MCP server exposing self-hosted Gitea (gitlab.impresion3d.pro) via stdio
JSON-RPC 2.0. Six tools: list_repos, get_repo, get_file, create_issue,
list_open_prs, merge_pr. The merge_pr tool pins the Gitea 'do'-field trap
(see skill gitea pitfall #13) — sends lowercase 'do', treats HTTP 200
with non-empty body as failure.

Stack: FastMCP + httpx + pydantic + uv, with respx-mocked pytest suite
(15 tests, 0.69s) and ruff + mypy strict green.

Verification: scripts/verify_stdio.py boots the server as a subprocess,
exchanges real MCP messages over stdio, and calls list_repos against the
live Gitea instance — all green.
2026-07-22 12:47:59 +00:00

131 lines
4.9 KiB
Markdown

# homelab-mcp-gitea
A **Model Context Protocol (MCP) server** that exposes a self-hosted **Gitea**
instance as a set of tools, so any MCP-aware client (Claude Desktop, Hermes
Agent, mcp-cli, the MCP Inspector) can list repos, fetch files, open issues,
list PRs, and merge PRs through natural language — without hand-rolling
`curl` invocations every time.
This is the **first** package of a planned `homelab-mcp-*` family:
`homelab-mcp-gitea``homelab-mcp-portainer``homelab-mcp-nas` … each one
a thin MCP wrapper over an existing self-hosted service.
## What is MCP, in one paragraph
MCP (Model Context Protocol) is the open standard (Anthropic, late 2024) that
lets an LLM client talk to "tool servers" the same way regardless of which
host it runs on. A server exposes:
- **tools** (named functions with typed arguments and return values)
- **resources** (named blobs the client can read)
- **prompts** (templated prompts the client can render)
…over **stdio** or **HTTP+SSE**, using **JSON-RPC 2.0** as the wire format.
The protocol is deliberately minimal so any language can implement it; the
Python and TypeScript SDKs are the most mature.
In this project, each `GiteaClient` method becomes an MCP `tool` decorated
with `@mcp.tool()`. The server runs over stdio (the default and simplest
transport); the client launches it as a subprocess and exchanges JSON-RPC
messages on stdin/stdout.
## Tools exposed
| Tool | What it does |
|-----------------|---------------------------------------------------------|
| `list_repos` | List repos visible to the authenticated user |
| `get_repo` | Get one repo's metadata |
| `get_file` | Read a file's text content (base64-decoded) |
| `create_issue` | Open a new issue on a repo |
| `list_open_prs` | List open pull requests for a repo |
| `merge_pr` | Merge a PR by index (with the Gitea `do` field trap fix)|
The `merge_pr` tool is non-trivial on purpose: Gitea's merge endpoint is
the famous `[Do]: Required` footgun (see the `gitea` skill, pitfall #13).
The client sends `{"do": "merge"}` (lowercase, two chars) and treats a
non-empty response body at HTTP 200 as failure. The tests pin both halves
of this contract so it can't regress.
## Setup
```bash
cd ~/homelab-mcp-gitea
uv sync --all-groups
cp .env.example .env
# Edit .env: set GITEA_URL and GITEA_TOKEN
uv run pytest # 14 tests, no network required
uv run ruff check src tests
uv run mypy src
```
## Running the server
```bash
# Foreground stdio transport (the default — what MCP clients expect)
uv run homelab-mcp-gitea
```
The server reads `GITEA_URL` and `GITEA_TOKEN` from the environment on every
tool call (no global state), so the same server process can be reused across
requests without leaking credentials between calls.
## Connecting from a client
### Claude Desktop (`~/.config/claude_desktop_config.json` on Linux)
```json
{
"mcpServers": {
"homelab-gitea": {
"command": "uv",
"args": ["--directory", "/home/caleidos/homelab-mcp-gitea", "run", "homelab-mcp-gitea"],
"env": {
"GITEA_URL": "https://gitlab.impresion3d.pro",
"GITEA_TOKEN": "<your-token>"
}
}
}
}
```
Restart Claude Desktop. The `homelab-gitea` server appears with its 6 tools
under the "tools" menu.
### MCP Inspector (browser-based debugger)
```bash
npx @modelcontextprotocol/inspector uv --directory ~/homelab-mcp-gitea run homelab-mcp-gitea
```
Opens a web UI where you can call each tool by hand and see the raw JSON-RPC.
## Architecture
```
src/homelab_mcp_gitea/
client.py # httpx-based Gitea REST client, no MCP knowledge
server.py # FastMCP server; thin wrapper that maps tools -> client calls
tests/
test_client.py # respx-mocked unit tests for every HTTP call
test_server.py # tool registration + routing smoke tests
```
The split is deliberate: `client.py` knows nothing about MCP, so it can be
reused from a plain Python REPL or a CLI, and `server.py` is small enough
to read in one sitting (~120 lines including docstrings).
## What's NOT here (and why)
- **No FastAPI / Celery / Alembic / Docker**. Those belong to the
`python-project-template-internal` stack, which targets deployable web
apps. An MCP server is a long-lived subprocess, not an HTTP service.
- **No CI-to-NAS deploy**. Local-only for v0.1; we can add Gitea Actions +
SSH deploy when there's a reason to keep the server running unattended.
- **No auth scopes, no rate limiting, no audit log**. The Gitea token
scopes are enforced server-side; this MCP just forwards.
## Next steps in the `homelab-mcp-*` family
- `homelab-mcp-portainer`: list containers, restart, tail logs, redeploy
stack. Requires the Portainer token + endpoint ID.
- `homelab-mcp-nas`: SSH-based filesystem + Container Station ops. The
trickier one (askpass / key auth, see `qnap-nas` skill pitfall J).