# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

MCP server that lets AI agents launch, interact with, and inspect Textual TUI applications headlessly via the Model Context Protocol. Uses Textual's built-in `Pilot` testing API to drive apps without a terminal.

## Commands

```bash
# Install (editable, with dev deps)
pip install -e ".[dev]"

# Run all tests (pytest-asyncio with auto mode, stops on first failure via -x)
pytest

# Run a single test file
pytest tests/test_session.py -v

# Run a single test
pytest tests/test_session.py::test_launch_and_stop -v

# Run the MCP server standalone
textual-mcp
```

## Architecture

**Entry point:** `src/textual_mcp/server.py` — FastMCP server with module-level tool registration. Tools are grouped into four categories registered via `_register_*` functions:
- Lifecycle (`textual_launch`, `textual_stop`) — inner-function closures over `SessionManager`
- Interaction (`textual_press`, `textual_click`, `textual_type_text`, `textual_hover`) — module-level async functions registered by reference
- Observation (`textual_snapshot`, `textual_screenshot`, `textual_query`, `textual_get_screen_stack`) — module-level async functions, read-only
- Assertion/Wait (`textual_wait_for`, `textual_check_errors`) — inner-function closures over `SessionManager`

**Session lifecycle:** `AppSession` wraps `App.run_test()` async context manager to get a persistent `Pilot` handle. The `app_context()` context manager sets Textual's `ContextVar`s (`active_app`, `active_message_pump`) required for pilot operations like `click` and `hover` that need app context in the current task.

**Serialization:** `WidgetTreeSerializer` walks the DOM producing indented text with `[ref=N]` markers on focusable widgets. `WidgetStateExtractor` uses a lazily-initialized ordered `isinstance` registry — ordering matters because some widgets inherit from others (e.g., `Label` before `Static`, `Checkbox` before `Static`).

**Error collection:** `ErrorCollector` hooks into Textual's message system via `message_hook` parameter on `run_test()`, capturing `Worker.StateChanged` errors without disrupting normal operation.

## Key Patterns

- **All tests are async** — `asyncio_mode = "auto"` in pyproject.toml, no need for `@pytest.mark.asyncio`
- **Test apps** live in `tests/apps/` and are added to `sys.path` in `conftest.py`
- **Tool return format:** All MCP tools return `json.dumps(...)` strings. Errors are returned as `{"error": str(e)}` rather than raised
- **Module-level singletons:** `mcp` (FastMCP), `_session_manager` (SessionManager), and `_extractor` (WidgetStateExtractor) are instantiated at module level
- **Hatchling build:** Source in `src/textual_mcp/`, entry point is `textual_mcp.server:main`

## Adding New Widget Types

To support a new widget type in `WidgetStateExtractor` (`src/textual_mcp/serializers/widget_state.py`):
1. Add a `_extract_<type>` instance method returning a `dict[str, Any]`
2. Add the widget import to `_init_extractors()`
3. Insert into `_EXTRACTORS` list — **place subclasses before parent classes**
