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
   ```