AGENTS.md · git:20260811.545f2ce · 2026-08-11 · sha256 86e7165ff1bbc152
AGENTS.md git:20260811.545f2ceA
Immutable. This exact content never changes and is served at /api/v1/blob/86e7165ff1bbc152.
# Gerrit MCP Server Development Guide
## Overview
This document serves as the comprehensive guide for developing, testing, and
maintaining the `gerrit-mcp-server`. It provides practical instructions for all
stages of the development lifecycle.
## Development Environment
### Prerequisites
- Python 3.12+
- [`uv`](https://docs.astral.sh/uv/getting-started/installation/) (the build
script will install it via `pip` if absent)
### Setup
The project uses `uv` for dependency management. `uv.lock` is committed to the
repository to ensure reproducible installs.
1. **Run the build script:**
```bash
./build-gerrit.sh
```
This will create a virtual environment in `.venv` and install all
dependencies (including dev extras) via `uv sync`.
2. **Activate the virtual environment:**
```bash
source .venv/bin/activate
```
> [!IMPORTANT] **ALWAYS** use the virtual environment's Python. Never use the
> system Python.
## Testing Standards
We use **pytest** as our testing framework.
### Structure
- **Files:** `test_*.py`
- **Functions:** Descriptive names like `test_query_changes_returns_results`.
- **Pattern:** Arrange, Act, Assert.
### Fixtures
- Use `pytest` fixtures for setup and dependency injection.
- Place shared fixtures in `conftest.py`.
- Use `unittest.mock.patch` as a context manager or fixture.
### Running Tests
Run tests from the project root (no venv activation needed):
```bash
uv run pytest
```
### Making any changes to source
If any changes to source are ever made, you must run `uv run pytest` to validate
that the changes did not break any tests. Ask the user first after any changes
are made.
## Code Style
Use the [`/astral:ruff` skill](https://github.com/astral-sh/claude-code-plugins)
if available. Otherwise, to fix and format:
```bash
uv run ruff check --fix . && uv run ruff format .
```
To check without modifying:
`uv run ruff check . && uv run ruff format --check .`
## Documentation
User-facing documentation lives in the `docs/` directory. Keep it in sync with
code changes — docs are part of the deliverable, not an afterthought.
### Layout
- **`docs/configuration.md`** — the `gerrit_config.json` schema and all
authentication methods.
- **`docs/available_tools.md`** — every MCP tool exposed by the server and what
it does.
- **`docs/testing.md`** — how to run unit, integration, and E2E tests.
- **`docs/best_practices.md`** — tips for using the server effectively.
- **`docs/use_cases.md`** — worked scenarios demonstrating the server.
- **`docs/gemini-cli.md`** — client setup for the Gemini CLI.
- **`docs/claude-code.md`** — client setup for Claude Code (plugin mode).
- **`docs/contributing.md`** / **`docs/code-of-conduct.md`** — contribution
guidelines and community standards.
### When to update docs
- **Add or change a tool** → update `docs/available_tools.md`.
- **Change config structure or auth** → update `docs/configuration.md`.
- **Change how tests are run** → update `docs/testing.md`.
- **Change behavior users rely on** → check `docs/best_practices.md` and
`docs/use_cases.md`.
### Adding, renaming, or removing a document
When you add, rename, or remove a file in `docs/`, cross-check the other
documentation files for references that need updating — in particular
`README.md` (which maintains its own index of doc links), `AGENTS.md`, and any
sibling docs that link to the changed file. Stale or broken cross-references
should be fixed in the same change.
### Style
Match the existing Markdown conventions: wrap prose at ~80 columns, use fenced
code blocks with language hints, and prefer `> [!IMPORTANT]`/`> [!NOTE]`
callouts as used elsewhere in this guide.
Enforce correct formatting by running mdformat before submitting:
```bash
uv run mdformat .
```
To check without modifying: `uv run mdformat --check .`
## Runtime & Configuration
### Configuration
The server is configured via `gerrit_mcp_server/gerrit_config.json`.
- **Environment Variable:** `GERRIT_CONFIG_PATH` can be used to point to a
custom config file.
- **Structure:**
```json
{
"gerrit_hosts": [
{
"name": "MyGerrit",
"external_url": "https://gerrit.example.com",
"authentication": { "type": "http_basic", ... }
}
]
}
```
*See [Configuration Guide](docs/configuration.md) for full details.*
### Client Setup (Gemini CLI)
To configure the Gemini CLI to use this server, see the
**[Gemini CLI Setup Guide](docs/gemini-cli.md)**.
### Running Locally
To run the server locally for debugging (no venv activation needed):
```bash
uv run gerrit-mcp-server
```
## Debugging
- **Logs:** The server outputs logs to stderr.
## Tool Design
### Structured Output
All tools should return a typed `TypedDict` rather than assembling
`[{"type": "text", "text": ...}]` content blocks manually. The MCP SDK
auto-generates an `output_schema` from the return annotation and produces both
structured and unstructured content automatically — structured for clients that
support it, text fallback for those that don't.
- Declare a `TypedDict` for the return type; use `Optional[X]` (not
`NotRequired`) for optional fields, and always include the key in every return
path (set to `None` when there is no value).
- Return the dict directly from the function.
- Raise exceptions on error rather than returning text error content blocks.
> [!IMPORTANT] Do **not** use `NotRequired` in tool-result `TypedDict`s — it
> causes the MCP SDK to raise validation errors either when annotations are
> stringized (`from __future__ import annotations`) or when any return path
> omits the key (validated as `None` against the declared type). Use
> `Optional[X]` and set `"key": None` on every path that has no value.
```python
from typing import List, Optional, TypedDict
class _ParentChange(TypedDict):
change_number: int
subject: str
work_in_progress: bool
class _MyToolResult(TypedDict):
change_id: str
items: List[_ParentChange]
note: Optional[str] # None unless there is something to say
@mcp.tool()
async def my_tool(
change_id: str,
gerrit_base_url: Optional[str] = None,
) -> _MyToolResult:
...
try:
raw = json.loads(await run_curl([url], base_url))
except json.JSONDecodeError as e:
raise ValueError(f"Failed to parse Gerrit response: {e}") from e
return {"change_id": change_id, "items": [...], "note": None}
```
See `get_commit_message` in `gerrit_mcp_server/main.py` for a complete example.
## Contributing
### Creating a CL
This project uses Gerrit for code reviews. The primary development branch is
`master`.
1. **Create a new branch:**
```bash
git checkout -b <your-feature-branch>
```
2. **Commit your changes:**
```bash
git commit -m "Your descriptive commit message"
```
3. **Push for review:** Changes must be pushed to `refs/for/master` to create a
CL. Direct pushes to `master` are not permitted.
```bash
git push origin HEAD:refs/for/master
```