Files
finn-mcp/IMPLEMENTATION.md
T
2026-05-16 06:54:17 +00:00

15 KiB
Raw Blame History

IMPLEMENTATION.md — Phase 2 build runbook

How to drive Phase 2 (the 12 steps in PRD.md §29) to completion using an AI agent. Each step has its own kickoff prompt, files affected, and "done" criteria. Run them in order. Each step is independently mergeable.


0. Pre-flight

Before starting step 1:

  1. ls -la

  2. Venv is healthy. From the project root:

    source .venv/bin/activate
    pytest -x                       # green except for any pre-existing FastMCP-related skips
    ruff check .                    # zero issues
    
  3. Docs are in place. Re-confirm PRD.md §17 (code ownership) is current — every step below references it.

If any of these fail, stop and fix before proceeding.


How to use this runbook

For each step:

  1. Create a feature branch: git checkout -b feat/phase2-step-<N>-<slug> off chore/cleanup-phase-2-prep.
  2. Open a fresh agent chat with repo access. Paste the kickoff prompt verbatim.
  3. Let the agent propose, implement, and test. Push back where it skips tests or violates §17.
  4. When all "done" boxes are checked, merge into chore/cleanup-phase-2-prep.
  5. Move to the next step.

Each kickoff prompt assumes the agent reads PRD.md, AGENTS.md, and the relevant instruction files first — that's encoded in the prompt.

After step 12, merge chore/cleanup-phase-2-prep into main.


Step 1 — Dev workflow already switched to local venv

This step is done by the time CLEANUP.md is merged. The instruction files and AGENTS.md already use local venv. Sanity check:

source .venv/bin/activate
which finn-eiendom 2>/dev/null || echo "expected: not yet installed; entry points come in steps 6 and 7"
ruff check .                 # zero issues
pytest -x                    # green (allow mcp_server failures)

Move on.


Step 2 — Pydantic v2 cleanup

Kickoff prompt

Read PRD.md (especially §17 code ownership and A8 acceptance criterion), .github/instructions/python.instructions.md, and .github/instructions/clean-code.instructions.md.

Implement Phase 2 step 2: convert every Pydantic model in finn_eiendom/models.py from v1 (class Config:) to v2 (model_config = ConfigDict(...)). Use context7:query-docs on pydantic/pydantic if you're not sure of the v2 syntax — don't guess.

Add tests/test_models.py with a JSON roundtrip test per model.

Run ruff check ., ruff format ., and pytest tests/test_models.py -v before declaring done.

Files

  • finn_eiendom/models.py (edit)
  • tests/test_models.py (new)

Done when

  • grep -rn "class Config:" finn_eiendom/ produces zero output.
  • pytest tests/test_models.py is green.
  • Existing tests still pass.

Step 3 — Service layer

Kickoff prompt

Read PRD.md §16 (Service layer) and §17 (code ownership), .github/instructions/python.instructions.md and .github/instructions/clean-code.instructions.md.

Create finn_eiendom/service.py with the public surface listed in PRD §16: get_or_fetch_ad, get_or_fetch_eiendom_unit, get_or_fetch_similar_units, analyze_search, analyze_ad, analyze_ad_against_comps, find_similar_to_liked, compare_ads, resolve_eiendom_unit_from_finn_url, build_unit_vector_for_unit_code, decode_unit_vector_to_dict, save_feedback, get_shortlist, get_new_ads_since_last_run.

Each function:

  1. Opens its own SQLite connection via cache.init_db(FINN_CACHE_PATH).
  2. Reads cache first with TTLs from config.py.
  3. On miss or force_refresh=True, calls the fetcher in ad.py / eiendom_no.py.
  4. Writes the fresh result back.
  5. Returns a typed model or dict.

Do not duplicate behavior from analysis.py — delegate to it. Add tests/test_service.py covering the five service tests listed in PRD §25.2.

Files

  • finn_eiendom/service.py (new)
  • tests/test_service.py (new)
  • tests/conftest.py (may need a tmp_db fixture if it doesn't exist)

Done when

  • pytest tests/test_service.py is green.
  • service.py imports only from models, config, cache, analysis, ad, eiendom_no, feedback, scoring, stdlib.
  • No import httpx or import sqlite3 outside their owners.

Step 4 — Formatting layer

Kickoff prompt

Read PRD.md §17.6 (shared formatting module) and §19 (output formats), .github/instructions/clean-code.instructions.md.

Create finn_eiendom/formatting.py with these renderers (signatures in PRD §17.6): render_ad, render_shortlist, render_comparison, render_diff, render_similar_units, render_unit, render_score_breakdown, plus render_cache_stats for the CLI cache subcommand.

Each renderer accepts (payload, fmt: Literal["json","markdown","table"]) -> str. Unsupported formats raise ValueError listing supported options. Table rendering only applies where it makes sense (shortlist, comparison, diff, similar-units).

Add tests/test_formatting.py covering the three tests listed in PRD §25.5.

Files

  • finn_eiendom/formatting.py (new)
  • tests/test_formatting.py (new)

Done when

  • pytest tests/test_formatting.py is green.
  • render_* is the only place that formats output. No inline rendering anywhere else (verified by reading diffs of steps 6 and 7).

Step 5 — HTTP retry on 5xx

Kickoff prompt

Read PRD.md A9 (acceptance criterion), .github/instructions/python.instructions.md.

Extend HTTPClient.get() in finn_eiendom/http.py to retry on 5xx responses (500/502/503/504) with exponential backoff 1s, 2s, 4s, up to retries attempts (default 3). Surface 4xx as httpx.HTTPStatusError immediately. Apply the existing request_delay between any two calls.

If you're unsure about httpx retry semantics or respx test patterns, use context7.

Add tests/test_http.py covering the three tests listed in PRD §25.6 using respx.

Files

  • finn_eiendom/http.py (edit)
  • tests/test_http.py (new)

Done when

  • pytest tests/test_http.py is green.
  • httpx imports remain confined to http.py.

Step 6 — Replace FastAPI with FastMCP

Kickoff prompt

Read PRD.md §14 (MCP design — every tool and input schema), §17 (code ownership), and .github/instructions/mcp.instructions.md end-to-end.

Rewrite finn_eiendom/mcp_server.py from scratch:

  • Use from mcp.server.fastmcp import FastMCP.
  • Configure stderr-only logging.
  • Register all 14 tools listed in PRD §14.1 with the finn_ prefix.
  • Each tool body has the shape in mcp.instructions.md §"Tool body shape": one service.<function> call, one formatting.render_* call, try/except returning the JSON error envelope.
  • Input schemas as in PRD §14.2.
  • Annotations: readOnlyHint=True for all except finn_save_feedback.
  • main() calls mcp.run(transport="stdio").
  • Add finn-eiendom-mcp = "finn_eiendom.mcp_server:main" to [project.scripts] in pyproject.toml.

If unsure about FastMCP annotations or transport options, use context7:query-docs on the MCP Python SDK.

Rewrite tests/test_mcp_server.py to cover the three tests in PRD §25.3. Use the SDK's testing helpers — do not spawn a subprocess.

Verify: finn-eiendom-mcp boots over stdio, mcp dev finn_eiendom/mcp_server.py lists all 14 tools.

Files

  • finn_eiendom/mcp_server.py (full rewrite)
  • tests/test_mcp_server.py (full rewrite)
  • pyproject.toml (edit [project.scripts])

Done when

  • mcp_server.py imports only service, formatting, models, config, stdlib, mcp, pydantic.
  • All 14 tools registered.
  • pytest tests/test_mcp_server.py is green.
  • grep -rn "FastAPI" finn_eiendom/ is empty.

Step 7 — CLI

Kickoff prompt

Read PRD.md §15 (CLI design — every command and option) and .github/instructions/cli.instructions.md end-to-end.

Create finn_eiendom/cli.py with a typer.Typer app exposing all commands in PRD §15.1, plus finn_eiendom/__main__.py that calls the app. Add to pyproject.toml:

[project.scripts]
finn-eiendom = "finn_eiendom.cli:app"

Each command:

  • Translates options into a service.<function> call.
  • Calls formatting.render_*(result, format) and typer.echo(...).
  • No business logic, no inline rendering.
  • Body under ~20 lines.

Sub-app for cache (stats/clear/clear-html/clear-json) and config (show/path). serve accepts --transport stdio|http and dispatches to mcp_server.main() or the HTTP transport.

If unsure about Typer sub-apps or CliRunner, use context7.

Add tests/test_cli.py covering the five tests in PRD §25.4 using typer.testing.CliRunner. Mock service.* with monkeypatch — do not exercise the full stack here, that's test_service.py.

Files

  • finn_eiendom/cli.py (new)
  • finn_eiendom/__main__.py (new)
  • tests/test_cli.py (new)
  • pyproject.toml (edit)

Done when

  • finn-eiendom --help lists every command in PRD §15.1.
  • cli.py imports only service, formatting, models, config, stdlib, typer.
  • pytest tests/test_cli.py is green.

Step 8 — Diff workflow (new / removed / changed)

Kickoff prompt

Read PRD.md §10.8, §13 (search_runs table), workflow I in §18, and .github/instructions/clean-code.instructions.md.

Implement:

  1. search_runs and scores tables in cache.py (use existing migration pattern).
  2. service.get_new_ads_since_last_run(search_url) that compares against the previous run for the same normalized_url and returns {new_ads, removed_ads, changed_ads} with price/common_costs/status diffs on changed.
  3. finn_get_new_ads_since_last_run MCP tool.
  4. finn-eiendom diff <url> CLI command.
  5. formatting.render_diff(result, fmt).

Add tests covering: empty previous-run case, all-new case, mixed new+removed+changed case.

Done when

  • The three new tests pass.
  • MCP and CLI both expose the same behavior with identical defaults.

Step 9 — Compare workflow

Kickoff prompt

Read PRD.md workflow K in §18 and §14.2 (CompareAdsInput).

Implement service.compare_ads(finnkoder, include_eiendom_no=True, include_comps=True) returning a comparison table + winners by category (best value / lifestyle / hybel / bargain / safest / highest risk / most overpriced).

Wire finn_compare_ads MCP tool and finn-eiendom compare <finnkode...> CLI command. Add formatting.render_comparison. Tests for service and CLI.

Done when

  • finn-eiendom compare 462400360 461153194 --format markdown produces a readable comparison.
  • Service test covers the winners-by-category logic.

Step 10 — Similar-to-liked

Kickoff prompt

Read PRD.md workflow G in §18 and FindSimilarToLikedInput in §14.2.

Implement service.find_similar_to_liked(finnkode, mode, listing_status):

  1. Load FinnAd; verify feedback has verdict=liked for this finnkode.
  2. Ensure Eiendom.no enrichment + unit_vector exist.
  3. Fetch similar-units (prefer FOR_SALE for recommendations, RECENTLY_SOLD for comps).
  4. Score candidates against user preferences.
  5. Return ranked recommendations.

Wire MCP tool and CLI command. Tests covering: no liked feedback raises clear error; happy path returns ranked list.

Done when

  • finn-eiendom similar-to-liked 462400360 returns ranked candidates when the listing has a liked verdict, and a clear error otherwise.

Step 11 — Architecture tests

Kickoff prompt

Read PRD.md A10 (architecture acceptance criterion) and §17.3 (layering invariants).

Create tests/test_architecture.py that walks every .py file under finn_eiendom/ with ast, collects all import and from X import Y statements, and asserts the layering invariants in PRD A10:

  • No httpx outside http.py.
  • No sqlite3 outside cache.py.
  • No BeautifulSoup outside search.py / ad.py.
  • No msgpack outside eiendom_no.py.
  • mcp_server.py and cli.py import only from the allowed set.
  • service.py never imports mcp_server or cli.

Add a parametrize'd test per invariant so failures show which module violated which rule. Failures should print the offending import line and module.

Done when

  • pytest tests/test_architecture.py is green.
  • Deliberately introducing a violation (e.g. import httpx in service.py) makes a test fail with a clear message.

Step 12 — README + Claude Desktop config + final verification

Kickoff prompt

Read PRD.md §21 (deployment), §22 (MVP scope), §24 (all acceptance criteria), README.md and USAGE.md.

Update README.md and USAGE.md so every command, env var, and Claude Desktop snippet matches what was actually built in steps 111. Verify with the user's exact paths.

Run the full A1A11 acceptance check:

  • A1: finn-eiendom-mcp boots over stdio; mcp dev finn_eiendom/mcp_server.py lists all 14 tools.
  • A2: finn-eiendom --help lists every §15.1 command; each command runs against fixtures.
  • A3 A9: matching service tests pass.
  • A10: pytest tests/test_architecture.py is green.
  • A11: ruff check . is clean; pytest is fully green; mypy --strict finn_eiendom passes or is documented as a gap.

Report any failures with specific file/line references — don't paper over them.

Files

  • README.md (edit to match reality)
  • USAGE.md (edit to match reality)

Done when

  • All 11 acceptance criteria in PRD §24 pass.
  • README + USAGE quickstart examples actually work end-to-end on a fresh clone.

Definition of done for the whole phase

Merge chore/cleanup-phase-2-prep into main when every box is checked:

  • All 12 steps merged in order.
  • finn-eiendom-mcp boots over stdio with all 14 tools.
  • finn-eiendom --help lists every command in PRD §15.1.
  • pytest is green, including the new test_service.py, test_cli.py, test_http.py, test_formatting.py, test_models.py, test_architecture.py.
  • ruff check . is clean.
  • mypy --strict finn_eiendom passes or has a documented exception list.
  • README.md and USAGE.md quickstart examples work on a fresh clone in under 5 minutes.
  • Claude Desktop config in USAGE.md is verified to work against your installation.

When a step blocks

If a step blocks on an unclear requirement:

  1. Re-read the relevant PRD section.
  2. Check PRD.md §28 (open questions) — the answer may be a deferred decision.
  3. If still unclear, write the question down, pick the simplest interpretation, mark it # TODO(<date>): revisit <question> in code, and move on.

If a step blocks on a library question (FastMCP, Pydantic v2, Typer, httpx, msgpack, respx):

  1. Use context7 — see .github/instructions/docs.instructions.md.
  2. If context7 returns nothing useful, write the smallest possible spike in scratch/ (gitignored) to verify behavior.

If a step blocks on §17 (code ownership) — i.e. it feels like the right answer requires putting logic in the "wrong" place:

  1. Stop.
  2. Re-read PRD §17.2 (decision table) and §17.3 (layering invariants).
  3. Ask whether the service layer is actually missing a function. Usually it is.
  4. Add the missing service function instead of bending the layering.