# PROJECT.md — module map The repo at a glance. For the why and the rules, read [`PRD.md`](PRD.md) §12 and §17. For the workflow, read [`AGENTS.md`](AGENTS.md). --- ## Source tree ``` finn-mcp/ ├── pyproject.toml ├── Makefile ├── README.md ← user-facing overview ├── USAGE.md ← full user guide ├── PRD.md ← product spec + architecture (§17 = constitution) ├── PROJECT.md ← this file ├── AGENTS.md ← workflow for AI agents and contributors ├── CLEANUP.md ← pre-Phase-2 cleanup runbook ├── IMPLEMENTATION.md ← Phase 2 build runbook (12 steps) │ ├── .github/ │ ├── copilot-instructions.md │ └── instructions/ │ ├── python.instructions.md │ ├── mcp.instructions.md │ ├── cli.instructions.md │ ├── tests.instructions.md │ ├── clean-code.instructions.md │ └── docs.instructions.md ← context7 lookup rules │ ├── finn_eiendom/ ← the package │ ├── __init__.py │ ├── __main__.py ← python -m finn_eiendom → CLI │ ├── config.py ← env vars, defaults, TTLs │ ├── models.py ← Pydantic v2 models │ ├── parser.py ← Norwegian number/area/URL/finnkode normalization │ ├── http.py ← async httpx client w/ retry + delay │ ├── cache.py ← SQLite schema + persistence │ ├── search.py ← FINN search HTML parsing │ ├── ad.py ← FINN listing HTML parsing │ ├── eiendom_no.py ← Eiendom.no unit search/detail, unit_vector, comps │ ├── scoring.py ← score model + classifications │ ├── feedback.py ← verdicts + soft preference signal │ ├── analysis.py ← shortlist + summary assembly │ ├── service.py ← get_or_fetch_* + thin facade for MCP and CLI │ ├── formatting.py ← render_* helpers (json/markdown/table) — shared by MCP and CLI │ ├── mcp_server.py ← FastMCP wrappers around service.py │ └── cli.py ← typer wrappers around service.py │ ├── tests/ │ ├── conftest.py │ ├── fixtures.py │ ├── fixtures/ ← HTML + JSON samples │ ├── test_parser.py │ ├── test_search.py │ ├── test_ad.py │ ├── test_eiendom_no.py │ ├── test_scoring.py │ ├── test_cache.py │ ├── test_http.py ← retry + delay behavior │ ├── test_service.py ← get_or_fetch_* + analyze_* │ ├── test_formatting.py ← render_* roundtrips │ ├── test_models.py ← Pydantic v2 roundtrips │ ├── test_mcp_server.py ← tool registration + error envelope │ ├── test_cli.py ← Typer CliRunner │ └── test_architecture.py ← import-graph invariants (PRD A10) │ └── data/ ← gitignored; SQLite cache lives here └── finn.sqlite ``` --- ## Module responsibilities Single-home rule: every concern lives in exactly one module. See `PRD.md` §17.2 for the full table. | Module | Owns | Imports allowed | | --------------- | --------------------------------------------------------------------- | ---------------------------------------------------------- | | `config.py` | env-var loading, defaults, TTL constants | stdlib | | `models.py` | Pydantic v2 models | stdlib, `pydantic` | | `parser.py` | Norwegian text normalization (numbers, dates, URLs, finnkode) | stdlib | | `http.py` | async `httpx.AsyncClient`, retry on 5xx, delay, user-agent | stdlib, `httpx` | | `cache.py` | SQLite schema, reads, writes, TTL | stdlib, `sqlite3`, `models` | | `search.py` | FINN search HTML → cards (BeautifulSoup) | stdlib, `bs4`, `parser`, `http`, `cache`, `models` | | `ad.py` | FINN listing HTML → `FinnAd` (BeautifulSoup) | stdlib, `bs4`, `parser`, `http`, `cache`, `models` | | `eiendom_no.py` | Eiendom.no unit search/detail, unit_vector, similar-units (msgpack) | stdlib, `msgpack`, `http`, `cache`, `models` | | `scoring.py` | 9 score components, total clamping, category classifier | stdlib, `models` | | `feedback.py` | feedback storage and retrieval | stdlib, `cache`, `models` | | `analysis.py` | shortlist + summary assembly | stdlib, `search`, `ad`, `eiendom_no`, `scoring`, `feedback`| | `service.py` | cache-aware orchestration; the only place that combines fetch + cache | stdlib, `config`, `cache`, `analysis`, `ad`, `eiendom_no`, `feedback`, `scoring`, `models` | | `formatting.py` | render_* helpers (json/markdown/table) | stdlib, `models` | | `mcp_server.py` | FastMCP tool definitions, error wrapping, stdio/HTTP entry | stdlib, `mcp`, `pydantic`, `service`, `formatting`, `config`, `models` | | `cli.py` | typer command definitions, --format dispatch | stdlib, `typer`, `service`, `formatting`, `config`, `models` | `mcp_server.py` and `cli.py` are siblings — they never import each other. `service.py` never imports `mcp_server` or `cli`. `tests/test_architecture.py` enforces all of this. --- ## Entry points Defined in `pyproject.toml`: ```toml [project.scripts] finn-eiendom-mcp = "finn_eiendom.mcp_server:main" finn-eiendom = "finn_eiendom.cli:app" ``` So you have: * `finn-eiendom-mcp` — MCP server over stdio (what Claude Desktop calls). * `finn-eiendom` — CLI with all subcommands. * `python -m finn_eiendom` — same as `finn-eiendom` (via `__main__.py`). * `import finn_eiendom` — the library, for tests and notebooks. --- ## Dependency graph ``` cli.py mcp_server.py ↓ ↓ └──> formatting.py <──┘ │ ↓ service.py ↓ analysis.py ↓ ┌───────────┼──────────────┐ ↓ ↓ ↓ search.py ad.py eiendom_no.py scoring.py feedback.py │ │ │ │ │ ↓ ↓ ↓ ↓ ↓ parser.py parser.py cache.py models.py cache.py │ │ │ ↓ ↓ ↓ http.py http.py http.py ``` Bottom layer: `parser.py`, `http.py`, `cache.py`, `models.py`, `config.py`. They depend only on stdlib + one third-party library each. The graph is acyclic and points downward. Every arrow can be drawn; no arrow can be drawn upward. --- ## Where to add things | You want to… | Add it to… | | ----------------------------------------- | --------------------------------------- | | Parse a new FINN field | `ad.py` or `search.py` + `models.py` | | Add a new score component | `scoring.py` | | Add a new env var | `config.py` | | Add a new MCP tool | `mcp_server.py` (after `service.py`) | | Add a new CLI command | `cli.py` (after `service.py`) | | Change how something renders | `formatting.py` | | Add a new orchestration / workflow | `service.py` (then add MCP + CLI) | | Speak to a new external API | new module next to `eiendom_no.py` | | Add a new SQLite table | `cache.py` | For anything else — read `PRD.md` §17.2 and §17.7.