Files
gitea-platform-mcp/CLAUDE.md
T
a-limasov-ii a6162a855c test: add 106 unit + smoke tests, 93% coverage
- pytest + respx + pytest-cov added as dev dependencies
- tests/smoke/test_registration.py: verify all 66 tools registered and unique
- tests/unit/: 12 files covering every tool — HTTP method, URL, params/body, errors
- tests/config.test.json + conftest.py: zero-network test setup via GITEA_CONFIG
- CLAUDE.md: developer guide with testing philosophy, pyramid, and commands
- server/gitea_mcp.py: get_commit, list_pr_files tools added
- README.md: tool count corrected to 66
2026-06-08 09:09:44 +03:00

5.3 KiB

CLAUDE.md — Developer Guide

Project overview

MCP server exposing Gitea REST API as 66 tools. Single source file: server/gitea_mcp.py. Config in config.json (gitignored). Run via uv run server/gitea_mcp.py.

Commit style

  • Conventional commits: feat:, fix:, chore:, docs:, test:
  • No Co-Authored-By or any AI attribution lines in commit messages

Adding tools

Each tool is a @mcp.tool() function. Pattern:

@mcp.tool()
def my_tool(owner: str, repo: str, instance: str = "") -> Any:
    """One-line docstring — becomes the tool description shown to the LLM."""
    return GET(instance, f"/repos/{owner}/{repo}/something")

Rules:

  • instance: str = "" is always the last parameter
  • Use _strip({...}) to drop None values before sending
  • URL-encode path segments that may contain slashes via _b(value)
  • Update the tool count in README.md after adding tools

Testing

Philosophy

The server has no business logic — every tool is a thin mapping from MCP arguments to a Gitea REST call. Tests therefore focus on contract correctness:

  • correct HTTP method and URL
  • correct request body / query parameters
  • correct handling of API error responses
  • all tools are registered and publicly discoverable

We do not test Gitea's own behavior — that is covered by Gitea's own test suite.

Test pyramid

┌──────────────────────────────┐
│  Integration (opt-in)        │  real Gitea via Docker Compose
├──────────────────────────────┤
│  Unit — per-tool contracts   │  pytest + respx (httpx mock)
├──────────────────────────────┤
│  Smoke — registration check  │  import + introspect registered tools
└──────────────────────────────┘

Tooling

Tool Purpose
pytest test runner
respx mock httpx.Client at the transport level — no monkey-patching
pytest-cov coverage reporting

Install dev dependencies:

uv add --dev pytest respx pytest-cov

Directory layout

tests/
├── conftest.py          # shared fixtures (mock transport, config override)
├── smoke/
│   └── test_registration.py   # all tools are registered, count matches README
└── unit/
    ├── test_meta.py
    ├── test_repos.py
    ├── test_branches.py
    ├── test_files.py
    ├── test_issues.py
    ├── test_labels_milestones.py
    ├── test_pull_requests.py
    ├── test_releases.py
    ├── test_webhooks.py
    ├── test_users_orgs.py
    ├── test_commits.py
    └── test_notifications.py

What each unit test must cover

For every tool function, write at minimum:

  1. Happy path — correct HTTP method, URL pattern, and response forwarded as-is
  2. Parameter mapping — optional params omitted when empty, present when set
  3. Error propagation — non-2xx status raises RuntimeError with the status code
# Example pattern
import respx, httpx, pytest
from server.gitea_mcp import get_commit

@respx.mock
def test_get_commit_happy_path():
    route = respx.get(
        "https://gitea.example.com/api/v1/repos/owner/repo/git/commits/abc123"
    ).mock(return_value=httpx.Response(200, json={"sha": "abc123"}))

    result = get_commit("owner", "repo", "abc123")

    assert route.called
    assert result["sha"] == "abc123"

@respx.mock
def test_get_commit_api_error():
    respx.get(...).mock(return_value=httpx.Response(404, json={"message": "not found"}))
    with pytest.raises(RuntimeError, match="404"):
        get_commit("owner", "repo", "deadbeef")

Smoke test: tool registration

# tests/smoke/test_registration.py
from server.gitea_mcp import mcp

EXPECTED_TOOL_COUNT = 66  # update when adding tools

def test_all_tools_registered():
    tools = mcp.list_tools()  # or equivalent FastMCP introspection
    assert len(tools) == EXPECTED_TOOL_COUNT

Update EXPECTED_TOOL_COUNT whenever you add or remove tools. If the count drifts from the README table, the smoke test catches it.

Running tests

# All tests
uv run pytest

# With coverage
uv run pytest --cov=server --cov-report=term-missing

# Smoke only
uv run pytest tests/smoke/

# One module
uv run pytest tests/unit/test_commits.py -v

Coverage target

  • Unit tests: 90 % line coverage on server/gitea_mcp.py
  • Smoke tests: 100 % tool registration (no gaps)
  • Integration tests are opt-in (require GITEA_TEST_URL + GITEA_TEST_TOKEN env vars)

Integration tests (opt-in)

Integration tests live in tests/integration/ and are skipped unless the environment variables GITEA_TEST_URL and GITEA_TEST_TOKEN are set. Use a dedicated test organisation/user on a throwaway Gitea instance — tests create and delete real resources.

import pytest, os

pytestmark = pytest.mark.skipif(
    not os.environ.get("GITEA_TEST_URL"),
    reason="integration tests require GITEA_TEST_URL"
)

For local development, a Docker Compose file (docker-compose.test.yml) can spin up a fresh Gitea instance automatically.