Adapted from python-project-template-internal/init.sh but trimmed to what applies to an MCP server: no Docker, no CI-to-NAS, no deploy step (see './init.sh help' for rationale). Single dev dependency group instead of separate lint/type/tests groups, since this project doesn't need that split. Verifies: uv → Python → uv sync → ruff check → ruff format --check → mypy → pytest. Exit 0 = ready; exit 1 = stop and fix.
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
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
# 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)
{
"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)
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-internalstack, 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, seeqnap-nasskill pitfall J).