# Development Guide

A comprehensive guide for developers contributing to foundry-mcp.

## Table of Contents

- [Project Overview](#project-overview)
- [Development Setup](#development-setup)
- [Architecture](#architecture)
- [Code Organization](#code-organization)
- [Adding New Tools](#adding-new-tools)
- [Response Contract](#response-contract)
- [Testing](#testing)
- [CLI Development](#cli-development)
- [Common Workflows](#common-workflows)
- [Batch Operations & Autonomous Mode](#batch-operations--autonomous-mode)
- [Best Practices](#best-practices)
- [Troubleshooting](#troubleshooting)

---

## Project Overview

foundry-mcp is an MCP (Model Context Protocol) server that enables AI assistants to manage spec-driven development workflows. It provides 40+ tools for:

- **Spec Management** — Create, validate, and lifecycle specs
- **Task Operations** — Track progress, dependencies, and blockers
- **Code Intelligence** — Query codebase documentation and call graphs
- **Testing** — Run and discover tests with pytest integration
- **LLM Features** — AI-powered reviews, documentation, and PR generation

### Key Design Principles

1. **Transport-Agnostic Core** — Business logic lives in `foundry_mcp.core.*`, shared by both MCP server and CLI
2. **JSON-First Output** — All responses follow a standardized envelope for LLM consumption
3. **Graceful Degradation** — LLM features work without API keys, falling back to structural analysis
4. **Spec-Driven Workflow** — Changes are planned in specifications before implementation

---

## Development Setup

### Prerequisites

- Python 3.10 or higher
- Git
- pip or uv

### Installation

```bash
# Clone the repository
git clone https://github.com/tylerburleigh/foundry-mcp.git
cd foundry-mcp

# Install in development mode with test dependencies
pip install -e ".[test]"

# Verify installation
python -c "import foundry_mcp; print('foundry-mcp installed')"
```

### Environment Configuration

Create a `foundry-mcp.toml` in your project root (optional):

```toml
[workspace]
specs_dir = "./specs"

[logging]
level = "DEBUG"
structured = true

[workflow]
mode = "single"
auto_validate = true
journal_enabled = true
```

Or use environment variables:

| Variable | Description | Default |
|----------|-------------|---------|
| `FOUNDRY_MCP_SPECS_DIR` | Path to specs directory | Auto-detected |
| `FOUNDRY_MCP_LOG_LEVEL` | Logging level | INFO |
| `FOUNDRY_MCP_WORKFLOW_MODE` | Execution mode | single |
| `FOUNDRY_MCP_CONFIG_FILE` | Path to TOML config | Auto-detected |

### Running the Server

```bash
# Start MCP server (production mode)
foundry-mcp

# Start with debug logging
FOUNDRY_MCP_LOG_LEVEL=DEBUG foundry-mcp

# Run from source directly
python -m foundry_mcp.server
```

### MCP Development Mode

```bash
# Interactive testing with MCP CLI
mcp dev foundry-mcp

# Or with uvx
uvx --from mcp dev foundry-mcp
```

---

## Architecture

### Layered Architecture

```
┌─────────────────────────────────────────────────────────────┐
│                    MCP Server (server.py)                   │
│              FastMCP + Tool Registration                    │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│                    Tools Layer (tools/*.py)                  │
│         MCP Tool Implementations + Response Formatting       │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│                    Core Layer (core/*.py)                    │
│         Business Logic (transport-agnostic)                  │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│                    CLI Layer (cli/*.py)                      │
│         Click Commands + JSON Output                         │
└─────────────────────────────────────────────────────────────┘
```

### Layer Responsibilities

| Layer | Location | Responsibility |
|-------|----------|----------------|
| **Server** | `server.py` | MCP server creation, tool registration, entry point |
| **Tools** | `tools/*.py` | MCP tool implementations, parameter validation, response formatting |
| **Core** | `core/*.py` | Pure business logic, no transport dependencies |
| **CLI** | `cli/*.py` | Command-line interface using Click |

### Key Principle

**All business logic must live in `core/`**. Tools and CLI commands are thin wrappers that call core functions and format responses.

---

## Code Organization

```
src/foundry_mcp/
├── __init__.py
├── server.py              # MCP server entry point
├── config.py              # Configuration management
│
├── core/                  # Business logic (transport-agnostic)
│   ├── spec.py            # Spec file operations
│   ├── task.py            # Task operations
│   ├── journal.py         # Journal management
│   ├── lifecycle.py       # Spec lifecycle transitions
│   ├── validation.py      # Spec validation
│   ├── responses.py       # Response helpers (success_response, error_response)
│   ├── feature_flags.py   # Feature flag management
│   ├── pagination.py      # Cursor-based pagination
│   ├── observability.py   # Logging and metrics
│   └── ...
│
├── tools/                 # MCP tool implementations
│   ├── tasks.py           # Task-related tools
│   ├── validation.py      # Validation tools
│   ├── lifecycle.py       # Lifecycle tools
│   ├── journal.py         # Journal tools
│   ├── testing.py         # Test runner tools
│   ├── review.py          # LLM review tools
│   └── ...
│
├── cli/                   # Command-line interface
│   ├── main.py            # CLI entry point
│   ├── output.py          # JSON output formatting
│   ├── commands/          # Click command modules
│   │   ├── tasks.py
│   │   ├── lifecycle.py
│   │   └── ...
│   └── ...
│
├── resources/             # MCP resources (foundry:// URIs)
│   └── specs.py
│
└── prompts/               # MCP prompts (workflow templates)
    └── workflows.py
```

### Tool Module Structure

Each tool module follows this pattern:

```python
# tools/example.py

from dataclasses import asdict
from foundry_mcp.core.responses import success_response, error_response
from foundry_mcp.core.naming import canonical_tool

def register_example_tools(mcp, config):
    """Register example-related tools with the MCP server."""

    @mcp.tool()
    @canonical_tool("example-do-thing")
    def example_do_thing(param: str) -> dict:
        """
        Brief description of what this tool does.

        Args:
            param: Description of the parameter

        Returns:
            JSON object with result data
        """
        # Call core logic
        from foundry_mcp.core.example import do_thing
        result = do_thing(param)

        # Return standardized response
        return asdict(success_response(data={"result": result}))
```

---

## Adding New Tools

### Step 1: Implement Core Logic

Create or extend a module in `core/`:

```python
# core/example.py

def do_thing(param: str) -> dict:
    """
    Pure business logic with no transport dependencies.

    Args:
        param: Input parameter

    Returns:
        Result dictionary
    """
    # Implementation here
    return {"processed": param}
```

### Step 2: Create Tool Wrapper

Create or extend a module in `tools/`:

```python
# tools/example.py

from dataclasses import asdict
from typing import Optional

from foundry_mcp.core.responses import (
    success_response,
    error_response,
    validation_error,
    not_found_error,
)
from foundry_mcp.core.naming import canonical_tool

def register_example_tools(mcp, config):
    """Register example tools with the MCP server."""

    @mcp.tool()
    @canonical_tool("example-do-thing")
    def example_do_thing(
        param: str,
        optional_param: Optional[str] = None,
    ) -> dict:
        """
        Brief description (shown in tool discovery).

        More detailed description of what this tool does,
        when to use it, and what it returns.

        Args:
            param: Required parameter description
            optional_param: Optional parameter description

        Returns:
            JSON object with:
            - processed: The processed result
            - count: Number of items processed
        """
        # Validate inputs
        if not param:
            return asdict(validation_error(
                "param is required",
                field="param",
                remediation="Provide a non-empty param value",
            ))

        try:
            # Call core logic
            from foundry_mcp.core.example import do_thing
            result = do_thing(param)

            # Return success
            return asdict(success_response(
                data={
                    "processed": result["processed"],
                    "count": result.get("count", 0),
                },
            ))

        except FileNotFoundError:
            return asdict(not_found_error("Example", param))
        except Exception as e:
            return asdict(error_response(
                f"Failed to process: {e}",
                error_code="INTERNAL_ERROR",
                error_type="internal",
            ))
```

### Step 3: Register Tools

Add registration call in `server.py`:

```python
from foundry_mcp.tools.example import register_example_tools

def create_server(config: Optional[ServerConfig] = None) -> FastMCP:
    # ... existing code ...

    # Register tools
    register_example_tools(mcp, config)  # Add this line
```

### Step 4: Add Tests

Create tests in `tests/unit/`:

```python
# tests/unit/test_example.py

import pytest
from foundry_mcp.core.example import do_thing

def test_do_thing_basic():
    result = do_thing("input")
    assert result["processed"] == "input"

def test_do_thing_empty_input():
    with pytest.raises(ValueError):
        do_thing("")
```

### Naming Conventions

Follow the [naming conventions](../codebase_standards/naming-conventions.md):

| Surface | Name | Examples |
|--------|------|----------|
| Unified tool router | `spec` | `spec(action="list")`, `spec(action="validate")` |
| Unified tool router | `task` | `task(action="next")`, `task(action="prepare")` |
| Unified tool router | `lifecycle` | `lifecycle(action="activate")`, `lifecycle(action="archive")` |
| Unified tool router | `journal` | `journal(action="add")`, `journal(action="list")` |
| Unified tool router | `test` | `test(action="run", preset="full")`, `test(action="discover")` |
| Unified tool router | `review` | `review(action="spec")`, `review(action="fidelity")` |
| Unified tool router | `code` | `code(action="find-function")`, `code(action="trace")` |
| Unified tool router | `server` | `server(action="tools")`, `server(action="schema", tool_name="spec")` |

---

## Response Contract

All tool responses **must** use the standardized envelope:

```json
{
  "success": true,
  "data": { ... },
  "error": null,
  "meta": {
    "version": "response-v2",
    "request_id": "req_abc123",
    "warnings": ["..."],
    "pagination": { ... }
  }
}
```

### Using Response Helpers

Always use helpers from `core/responses.py`:

```python
from dataclasses import asdict
from foundry_mcp.core.responses import (
    success_response,
    error_response,
    validation_error,
    not_found_error,
    rate_limit_error,
)

# Success response
return asdict(success_response(
    data={"result": "value"},
    warnings=["Non-fatal warning"],
    pagination={"cursor": "abc", "has_more": True},
))

# Validation error
return asdict(validation_error(
    "Invalid email format",
    field="email",
    remediation="Use format: user@domain.com",
))

# Not found error
return asdict(not_found_error("Spec", "my-spec-id"))

# Generic error
return asdict(error_response(
    "Something went wrong",
    error_code="INTERNAL_ERROR",
    error_type="internal",
))
```

### Error Types

| error_type | HTTP Analog | When to Use | Retry? |
|------------|-------------|-------------|--------|
| `validation` | 400 | Invalid input | No |
| `not_found` | 404 | Resource missing | No |
| `conflict` | 409 | State conflict | Maybe |
| `rate_limit` | 429 | Too many requests | Yes |
| `internal` | 500 | Server error | Yes |
| `unavailable` | 503 | Service down | Yes |

See [Response Schema Guide](../codebase_standards/mcp_response_schema.md) for complete documentation.

---

## Testing

### Running Tests

```bash
# Run all tests
pytest

# Run with verbose output
pytest -v

# Run specific test file
pytest tests/unit/test_spec.py

# Run tests matching pattern
pytest -k "test_validation"

# Run only unit tests
pytest -m unit

# Run only integration tests
pytest -m integration

# Stop on first failure
pytest -x

# Show print statements
pytest -s
```

### Test Presets

| Preset | Use Case | Command Equivalent |
|--------|----------|-------------------|
| `quick` | Fast development feedback | `pytest -m "not slow" -x --timeout=60` |
| `full` | Complete test run | `pytest -v --timeout=300` |
| `unit` | Unit tests only | `pytest -m unit -v --timeout=120` |
| `integration` | Integration tests only | `pytest -m integration -v --timeout=300` |
| `smoke` | Quick verification | `pytest -m smoke -x --timeout=30` |

### Writing Tests

```python
# tests/unit/test_example.py

import pytest
import tempfile
from pathlib import Path

@pytest.fixture
def temp_specs_dir():
    """Create temporary specs directory structure."""
    with tempfile.TemporaryDirectory() as tmpdir:
        specs_dir = Path(tmpdir) / "specs"
        for status in ["pending", "active", "completed", "archived"]:
            (specs_dir / status).mkdir(parents=True)
        yield specs_dir

def test_example_success(temp_specs_dir):
    """Test successful operation."""
    # Arrange
    # ...

    # Act
    result = some_function()

    # Assert
    assert result["success"] is True
    assert "data" in result

@pytest.mark.slow
def test_expensive_operation():
    """Mark slow tests so they can be skipped during quick runs."""
    # ...

@pytest.mark.integration
def test_requires_external_service():
    """Mark integration tests that need external dependencies."""
    # ...
```

### Test Directory Structure

```
tests/
├── unit/                    # Unit tests for core modules
│   ├── test_spec.py
│   ├── test_task.py
│   ├── test_validation.py
│   └── ...
├── integration/             # Integration tests
│   ├── test_mcp_smoke.py
│   ├── test_mcp_tools.py
│   └── ...
└── fixtures/                # Test fixtures
    └── golden/              # Golden fixture files
```

See [Testing Guide](testing.md) for comprehensive testing documentation.

---

## CLI Development

### CLI Architecture

The CLI uses Click and outputs JSON only:

```python
# cli/commands/example.py

import click
from foundry_mcp.cli.output import output_json, output_error
from foundry_mcp.core.example import do_thing

@click.command()
@click.argument("param")
@click.option("--dry-run", is_flag=True, help="Preview without executing")
def example_command(param: str, dry_run: bool):
    """Brief description of the command."""
    try:
        result = do_thing(param, dry_run=dry_run)
        output_json({
            "success": True,
            "data": result,
            "error": None,
        })
    except Exception as e:
        output_error(str(e))
```

### CLI Output Rules

1. **JSON-only output** — No ANSI colors, progress bars, or human formatting
2. **Same response contract** — Use the same envelope as MCP tools
3. **Stderr for errors** — Non-JSON diagnostics go to stderr
4. **Exit codes** — 0 for success, non-zero for failures

### Session Context & Transcript Access

`session context` parses Claude transcript JSONL files to estimate token budgets.
Because those transcripts live outside the repo, access is opt-in:

- Pass `--transcript-dir <path>` to point at an explicit directory of `.jsonl` files
- Set `FOUNDRY_MCP_ALLOW_TRANSCRIPTS=1` or pass `--allow-home-transcripts` to scan
  `~/.claude/projects` derived paths
- Without either control, the command returns `TRANSCRIPTS_DISABLED` to honor
  the trust-boundary guidance in `dev_docs/mcp_best_practices/08-security-trust-boundaries.md`

All transcript scans continue to produce JSON envelopes and respect the
`MEDIUM_TIMEOUT` budget so that exponential backoff never exceeds the declared limit.

### Registering Commands

```python
# cli/main.py

from foundry_mcp.cli.commands.example import example_command

@click.group()
def cli():
    """Foundry CLI - Spec-driven development tools."""
    pass

cli.add_command(example_command)
```

---

## Common Workflows

### Adding a New Feature

1. **Create a spec** — Document the feature in `specs/pending/`
2. **Activate the spec** — Move to `specs/active/` when starting work
3. **Implement core logic** — Add to `core/*.py`
4. **Create tool wrapper** — Add to `tools/*.py`
5. **Register tools** — Update `server.py`
6. **Add tests** — Both unit and integration
7. **Update docs** — Response contract, tool discovery, guides
8. **Complete the spec** — Move to `specs/completed/`

### Debugging Issues

```bash
# Enable debug logging
FOUNDRY_MCP_LOG_LEVEL=DEBUG foundry-mcp

# Check configuration
python -c "from foundry_mcp.config import get_config; print(get_config())"

# Verify spec directory structure
ls -la specs/{pending,active,completed,archived}

# Run specific test with verbose output
pytest tests/unit/test_spec.py -v -s

# Check for import errors
python -c "from foundry_mcp.server import create_server; print('OK')"
```

### Code Review Checklist

- [ ] Uses standardized response envelope via helpers
- [ ] Validates inputs early with descriptive errors
- [ ] Handles errors gracefully with proper error types
- [ ] Includes appropriate logging
- [ ] Has unit test coverage
- [ ] Follows naming conventions
- [ ] Updates relevant documentation

---

## Batch Operations & Autonomous Mode

The task router supports batch operations for parallel task execution and autonomous mode for continuous task processing.

### Batch Actions

#### prepare-batch

Finds multiple independent tasks that can be executed in parallel.

```python
# Find up to 3 independent tasks for parallel execution
result = mcp__foundry_mcp__task(
    action="prepare-batch",
    spec_id="my-spec-001",
    max_tasks=3,          # Optional: default is 3
    token_budget=50000,   # Optional: max combined context tokens
)
```

Response includes:
- `tasks`: List of task contexts with dependencies, phase info, parent context
- `task_count`: Number of tasks returned
- `warnings`: List of warnings (e.g., logical coupling disclaimer)
- `stale_tasks`: Tasks stuck in `in_progress` for >1 hour
- `dependency_graph`: Visual representation of task relationships

Independent tasks must have:
- Different file paths (no conflicts)
- No dependency relationships between them
- Tasks without `file_path` are treated as exclusive barriers

#### start-batch

Atomically starts multiple tasks as `in_progress`. All-or-nothing validation.

```python
# Start tasks as in_progress atomically
result = mcp__foundry_mcp__task(
    action="start-batch",
    spec_id="my-spec-001",
    task_ids=["task-1-1", "task-1-2", "task-1-3"],
)
```

Returns:
- `started`: List of successfully started task IDs
- `started_count`: Number of tasks started
- `started_at`: ISO timestamp

Validation includes:
- All tasks must exist and be pending
- No dependency conflicts between tasks
- No file path conflicts

#### complete-batch

Completes multiple tasks with partial failure support.

```python
# Complete tasks with individual results
result = mcp__foundry_mcp__task(
    action="complete-batch",
    spec_id="my-spec-001",
    completions=[
        {"task_id": "task-1-1", "success": True, "completion_note": "Implemented feature X"},
        {"task_id": "task-1-2", "success": False, "completion_note": "Tests failing, needs investigation"},
        {"task_id": "task-1-3", "success": True, "completion_note": "Refactored module Y"},
    ],
)
```

Returns:
- `results`: Per-task status (completed, failed, skipped, error)
- `completed_count`: Number successfully completed
- `failed_count`: Number marked as failed

Failed tasks get `status: "failed"` with incremented `retry_count`.

#### reset-batch

Resets stale or specified `in_progress` tasks back to pending.

```python
# Auto-detect and reset stale tasks (>1 hour in progress)
result = mcp__foundry_mcp__task(
    action="reset-batch",
    spec_id="my-spec-001",
)

# Or reset specific tasks
result = mcp__foundry_mcp__task(
    action="reset-batch",
    spec_id="my-spec-001",
    task_ids=["task-1-1", "task-1-2"],
)
```

### Session Configuration

The `session-config` action manages ephemeral autonomous session state.

```python
# Get current session config
result = mcp__foundry_mcp__task(
    action="session-config",
    get=True,
)

# Enable autonomous mode
result = mcp__foundry_mcp__task(
    action="session-config",
    auto_mode=True,
)

# Disable autonomous mode
result = mcp__foundry_mcp__task(
    action="session-config",
    auto_mode=False,
)
```

Response includes:
- `session_id`: Unique session identifier
- `autonomous`: Session state with:
  - `enabled`: Whether autonomous mode is active
  - `tasks_completed`: Count of tasks completed in this session
  - `pause_reason`: Why auto-mode paused (if paused)
  - `started_at`: When autonomous mode was enabled

### Autonomous Mode Pause Triggers

Autonomous mode automatically pauses when guardrails are hit:

| Pause Reason | Trigger | Description |
|--------------|---------|-------------|
| `context` | Context usage >= 85% | Token/context budget nearing limit |
| `error` | >= 3 consecutive errors | Too many failures indicate investigation needed |
| `blocked` | All remaining tasks blocked | No actionable tasks available |
| `limit` | Session limit reached | Max consultations or tokens exceeded |
| `user` | User disabled auto-mode | Explicit user request to stop |

The `_check_autonomous_limits()` helper in `core/batch_operations.py` evaluates these conditions and updates `AutonomousSession.pause_reason`.

### Autonomous Mode Workflow Example

```python
# 1. Enable autonomous mode
mcp__foundry_mcp__task(action="session-config", auto_mode=True)

# 2. Loop: prepare and execute tasks
while True:
    # Get next task
    result = mcp__foundry_mcp__task(action="prepare", spec_id="my-spec")

    if result["data"].get("spec_complete"):
        print("Spec complete!")
        break

    # Check autonomous session hints
    auto_hints = result["data"].get("auto_mode_hints", {})

    # If high complexity or user input needed, pause
    if auto_hints.get("estimated_complexity") == "high":
        print("Complex task - requesting user confirmation")
        break

    if auto_hints.get("may_require_user_input"):
        print("Task may need user input")
        break

    # Get current session and check limits
    session = mcp__foundry_mcp__task(action="session-config", get=True)
    pause_reason = session["data"]["autonomous"].get("pause_reason")

    if pause_reason:
        print(f"Autonomous mode paused: {pause_reason}")
        break

    # Execute task...
    task_id = result["data"]["task_id"]
    mcp__foundry_mcp__task(action="start", spec_id="my-spec", task_id=task_id)

    # ... implement task ...

    mcp__foundry_mcp__task(
        action="complete",
        spec_id="my-spec",
        task_id=task_id,
        completion_note="Task completed successfully",
    )

# 3. Disable autonomous mode when done
mcp__foundry_mcp__task(action="session-config", auto_mode=False)
```

---

## Best Practices

### General

1. **Keep tools thin** — Business logic belongs in `core/`, not `tools/`
2. **Validate early** — Check inputs before calling core functions
3. **Use response helpers** — Never construct response dicts manually
4. **Document thoroughly** — Tool docstrings appear in discovery
5. **Test in isolation** — Each test should be independent

### Response Handling

1. **Always return `success`** — Even for empty results
2. **Distinguish empty vs. not found** — Empty results are `success: true`
3. **Include remediation** — Error messages should be actionable
4. **Use warnings for non-fatal issues** — Don't fail on recoverable problems

### Performance

1. **Paginate large results** — Use cursor-based pagination
2. **Cache expensive operations** — Use `core/cache.py` utilities
3. **Mark slow tests** — Use `@pytest.mark.slow` decorator
4. **Avoid blocking I/O** — Use async patterns for external calls

### Security

1. **Validate all inputs** — Never trust user data
2. **Sanitize paths** — Prevent directory traversal
3. **Mask sensitive data** — API keys, tokens in logs
4. **Respect trust boundaries** — See [Security Guide](../mcp_best_practices/08-security-trust-boundaries.md)

---

## Troubleshooting

### Common Errors

| Error | Cause | Solution |
|-------|-------|----------|
| `ModuleNotFoundError: foundry_mcp` | Package not installed | `pip install -e .` |
| `No specs directory found` | Missing specs path | Set `FOUNDRY_MCP_SPECS_DIR` |
| `Spec not found: {id}` | Wrong spec ID or folder | Check spec exists in status folders |
| `Invalid spec structure` | Malformed JSON | Run `spec(action="validate")` |
| `pytest not found` | Missing test dependency | `pip install -e ".[test]"` |

### Getting Help

1. Check the [Testing Guide](testing.md) for test-related issues
2. Review [MCP Best Practices](../mcp_best_practices/README.md) for patterns
3. Search existing issues on GitHub
4. Create a new issue with reproduction steps

---

## Related Documentation

| Document | Description |
|----------|-------------|
| [Testing Guide](testing.md) | Complete testing documentation |
| [Response Schema](../codebase_standards/mcp_response_schema.md) | Response contract details |
| [Naming Conventions](../codebase_standards/naming-conventions.md) | Tool naming standards |
| [MCP Best Practices](../mcp_best_practices/README.md) | Industry patterns |
| [Architecture](../architecture/adr-001-cli-architecture.md) | CLI/MCP architecture decision record |
| [LLM Configuration](llm-configuration.md) | LLM provider setup |

---

*Last updated: 2026-01-02*
