# v1.2 Plan — Foundation Hardening

**Branch**: `feature/v1.2-foundation`
**Goal**: Break the 88KB `server.py` monolith into maintainable modules. Add one-command install. List on MCP registry.
**Gate**: All 396 tests must pass. No tool regressions. Tool count in `test_server.py` unchanged.

---

## Problem

`server.py` is 2,328 lines / 88KB containing 29 MCP tools. This is the root blocker for:
- Adding new tools (Codex-specific, media-specific) without making it worse
- Code review and onboarding (no one can read 88KB in one sitting)
- Parallel feature development (every feature branch touches the same file)
- Testing individual tool groups in isolation

---

## Architecture

### Before
```
src/llm_router/
├── server.py          # 2,328 lines — all 29 tools + all helpers
└── ...
```

### After
```
src/llm_router/
├── server.py          # ~120 lines — FastMCP init + register all modules
├── tools/
│   ├── __init__.py    # empty
│   ├── routing.py     # llm_classify, llm_route, llm_track_usage, llm_stream  (~400 lines)
│   ├── text.py        # llm_query, llm_research, llm_generate, llm_analyze, llm_code, llm_edit  (~400 lines)
│   ├── media.py       # llm_image, llm_video, llm_audio  (~250 lines)
│   ├── pipeline.py    # llm_orchestrate, llm_pipeline_templates  (~200 lines)
│   ├── admin.py       # llm_set_profile, llm_usage, llm_cache_stats, llm_cache_clear,
│   │                  # llm_quality_report, llm_health, llm_providers, llm_save_session  (~400 lines)
│   ├── subscription.py# llm_check_usage, llm_update_usage, llm_refresh_claude_usage  (~250 lines)
│   ├── codex.py       # llm_codex  (~200 lines)
│   └── setup.py       # llm_setup, llm_rate  (~350 lines)
└── ...
```

---

## Registration Pattern

FastMCP registers tools at decoration time. To split across files while sharing a single `mcp` instance, each module exposes a `register(mcp)` function that applies `@mcp.tool()` decorators:

```python
# tools/text.py
from mcp.server.fastmcp import FastMCP

def register(mcp: FastMCP) -> None:
    @mcp.tool()
    async def llm_query(prompt: str, ...) -> str:
        ...

    @mcp.tool()
    async def llm_research(prompt: str, ...) -> str:
        ...
```

```python
# server.py (after)
from mcp.server.fastmcp import FastMCP
from llm_router.tools import routing, text, media, pipeline, admin, subscription, codex, setup

mcp = FastMCP("llm-router")

routing.register(mcp)
text.register(mcp)
media.register(mcp)
pipeline.register(mcp)
admin.register(mcp)
subscription.register(mcp)
codex.register(mcp)
setup.register(mcp)
```

**Why this pattern**:
- Single `mcp` instance (required — FastMCP doesn't support merging)
- Each module is independently readable and testable
- No public API changes — tool names and signatures unchanged
- Backward compatible — existing `from llm_router.server import mcp` still works

---

## Helper Functions in server.py

`server.py` currently contains helper functions used across tools:
- `get_active_profile()` — move to `llm_router/state.py` (new thin module)
- `_check_tier()` — move alongside `get_active_profile()`
- `_active_profile` module-level var — move to `state.py`

All tools import from `llm_router.state` instead of `llm_router.server`.

---

## `llm-router install` CLI Command

### Current situation
Installing requires manual shell commands:
```bash
install -m 755 src/llm_router/hooks/auto-route.py ~/.claude/hooks/auto-route.py
install -m 755 src/llm_router/hooks/usage-refresh.py ~/.claude/hooks/usage-refresh.py
```

### After
```bash
llm-router install          # installs hooks + rules
llm-router install --check  # shows what would be installed
llm-router install --force  # overwrites existing
```

**Implementation**: Add `install` subcommand to the existing CLI entry point in `pyproject.toml`. Wraps existing `install_hooks.py` logic.

```python
# src/llm_router/__main__.py  (new file, or extend existing CLI)
def main():
    if sys.argv[1:] == ["install"] or "--install" in sys.argv:
        from llm_router.install_hooks import install_all
        install_all()
```

---

## MCP Registry Submission

The MCP registry at `registry.modelcontextprotocol.io` has no routing/orchestration servers — this is a gap.

Create `mcp-registry.json` at repo root:
```json
{
  "name": "llm-router",
  "description": "Subscription-aware LLM routing for Claude Code — routes tasks to 20+ providers with pressure-based fallback chains, Codex integration, and cost tracking",
  "version": "1.2.0",
  "homepage": "https://github.com/ypollak2/llm-router",
  "categories": ["routing", "cost-optimization", "claude-code"],
  "installation": {
    "pip": "pip install claude-code-llm-router",
    "setup": "llm-router install"
  },
  "tools": [
    "llm_route", "llm_classify", "llm_query", "llm_code", "llm_analyze",
    "llm_research", "llm_generate", "llm_edit", "llm_image", "llm_video",
    "llm_audio", "llm_orchestrate", "llm_codex", "llm_usage", "llm_health"
  ]
}
```

---

## Implementation Order

1. **Create `tools/` directory structure** — empty files, `__init__.py`
2. **Extract `state.py`** — `get_active_profile`, `_check_tier`, `_active_profile`
3. **Extract `tools/routing.py`** — 4 tools (smallest, good warmup)
4. **Extract `tools/subscription.py`** — 3 tools (self-contained)
5. **Extract `tools/media.py`** — 3 tools (media.py already exists, minimal deps)
6. **Extract `tools/pipeline.py`** — 2 tools
7. **Extract `tools/codex.py`** — 1 tool (just `llm_codex`)
8. **Extract `tools/text.py`** — 6 tools (largest, most complex)
9. **Extract `tools/admin.py`** — 8 tools
10. **Extract `tools/setup.py`** — 2 tools (`llm_setup` is the most complex ~700 lines)
11. **Rewrite `server.py`** as thin entrypoint
12. **Add `llm-router install` CLI**
13. **Add `mcp-registry.json`**
14. **Run full test suite** — must be 396/396

---

## Risk: `llm_setup` size

`llm_setup` is a single tool with ~700 lines of internal logic (6 subcommands: status, guide, discover, add, test, provider). It will be extracted into `tools/setup.py` as-is — no further decomposition in v1.2. Internal subcommand refactor is v1.3 scope.

---

## Test Strategy

After each extraction step:
```bash
uv run pytest tests/ -q --ignore=tests/test_integration.py -x
```

Full suite before merge:
```bash
uv run pytest tests/ -q --ignore=tests/test_integration.py
uv run ruff check src/llm_router/
```

`test_server.py` checks that all expected tool names are registered — this is the regression guard.

---

## Definition of Done

- [ ] `server.py` is ≤150 lines
- [ ] All 29 tools split across 8 `tools/` modules
- [ ] `llm-router install` works from `pip install`
- [ ] `mcp-registry.json` exists at repo root
- [ ] 396/396 tests pass
- [ ] `ruff check` clean
- [ ] `pyproject.toml` version = `1.2.0`
- [ ] `CHANGELOG.md` updated
- [ ] `TODO.md` tasks marked ✅
