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.
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user