# CLAUDE.md

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

## Build & Development Commands

```bash
# Install dependencies (use uv - recommended)
uv sync

# Or with pip
pip install -e ".[dev]"

# Run tests
pytest
pytest --cov=pydanticai_multiagent      # with coverage
pytest tests/agents/test_research.py -v  # single file

# Linting and type checking
ruff check src tests
ruff format src tests
mypy src

# Run the API server
pydanticai-platform serve
pydanticai-platform serve --reload       # with hot reload
uvicorn pydanticai_multiagent.api.app:app --reload  # direct uvicorn

# CLI commands
pydanticai-platform chat                 # interactive mode
pydanticai-platform query "your question"  # single query
pydanticai-platform db-migrate           # run database migrations

# MCP server (for Claude Desktop, Cursor, etc.)
pydanticai-platform mcp-serve            # stdio transport (default)
pydanticai-platform mcp-serve --transport sse --port 8001  # SSE transport
```

## Architecture

### Multi-Agent System

Router-based delegation pattern where `router_agent` analyzes requests and delegates to specialized agents:

- **Router** (`agents/router.py`): Lightweight gpt-4o-mini model for routing decisions
- **Specialists**: Research, Analyst, Code, Writer, Support - each with domain-specific tools and output models

Agents are module-level singletons with typed deps and outputs:
```python
research_agent: Agent[SearchDeps, ResearchResult] = Agent(...)
```

### Tool Composition with Toolsets

Tools are organized into `FunctionToolset`s and combined using `CombinedToolset` with prefixes:

```python
# In toolsets/research.py
research_toolset = CombinedToolset([
    web_search_toolset.prefixed("web"),    # tools become web_search, web_fetch_webpage
    vector_search_toolset.prefixed("kb"),  # tools become kb_search_documents
    common_toolset,                        # no prefix
])
```

Tools receive typed context via `RunContext[DepsType]` to access dependencies.

### Dependency Injection

Dataclass-based DI with protocol abstractions (`dependencies/base.py`):

- `BaseDeps`: Core infrastructure (http_client, db, cache, user_id)
- `SearchDeps(BaseDeps)`: Adds vector_store, search_api_key
- `AuthDeps(BaseDeps)`: Adds user_roles, permissions

When delegating between agents, convert deps to match target agent's requirements.

### Structured Outputs

All agents return Pydantic models (`models/`):
- `ResearchResult`: answer, sources, confidence, follow_up_questions
- `AnalysisResult`: summary, confidence, data_points, recommendations
- Models have helper methods like `format_with_sources()`

## Testing Patterns

Use `TestModel` for deterministic agent testing without API calls:

```python
from pydantic_ai.models.test import TestModel

async def test_agent():
    with research_agent.override(model=test_model):
        result = await research_agent.run("query", deps=deps)
```

Fixtures in `tests/conftest.py` provide mocked dependencies (`base_deps`, `search_deps`, `auth_deps`).

## MCP Integration

The platform has two MCP capabilities:

### MCP Server (Expose Platform)
Located in `mcp/server.py` - exposes platform as an MCP server for Claude Desktop, Cursor, etc.
- Tools: `chat`, `list_agents`, `get_usage`, `list_conversations`, `get_conversation`, `clear_conversation`
- Auth via `api_key` parameter on each tool call
- Mounted at `/mcp` on the FastAPI app

### MCP Client (Use External Servers)
Located in `mcp/client.py` - connects to external MCP servers as toolsets.
- Configure via `MCP_SERVERS` env var (JSON array)
- Supports stdio, SSE, and HTTP transports
- Toolsets automatically passed to all agent runs

```python
# Example MCP_SERVERS config
MCP_SERVERS='[{"type": "stdio", "command": "uvx", "args": ["mcp-server-time"], "prefix": "time"}]'
```

## Key Conventions

- Tools access deps via `ctx.deps.property`, never global state
- Pass `usage=ctx.usage` when delegating to sub-agents for cost tracking
- Use `Agent.override()` context manager for testing/model swapping
- pytest runs with `asyncio_mode = "auto"` - async tests work without decorators
