6.7 KiB
Copilot instructions for finn-eiendom-mcp
This project is a private, self-hosted Python platform for analyzing FINN real-estate listings. It exposes the same code through three coordinated front ends:
- A Python library (
finn_eiendom) — source of truth. - An MCP server (FastMCP, stdio + optional HTTP) over
finn_eiendom/mcp_server.py. - A CLI (
finn-eiendom) overfinn_eiendom/cli.py.
All three share the same service.py, formatting.py, cache.py, and models.py. Code lives in exactly one place and is called from both front ends. See PRD.md §17 for the full ownership rules — that section is the constitution.
Source of truth
Read in this order:
PRD.md— product and architecture, especially §17.PROJECT.md— module map.AGENTS.md— workflow..github/instructions/*.md— per-topic rules.
Module layout
finn_eiendom/
config.py # env vars, defaults, TTLs
models.py # Pydantic v2 models
parser.py # number/area/date/URL/finnkode normalization
http.py # async HTTP (httpx) with delay + retry + user-agent
cache.py # SQLite (sqlite3) schema + persistence
search.py # FINN search HTML parsing + pagination
ad.py # FINN listing HTML parsing
eiendom_no.py # Eiendom.no unit search/detail, unit_vector, similar-units
scoring.py # score model + classifications
feedback.py # verdicts + soft preference signal
analysis.py # orchestration + shortlist + summary
service.py # get_or_fetch_* + thin facade for MCP and CLI
formatting.py # render_* helpers shared by MCP and CLI
mcp_server.py # FastMCP wrappers around service.py
cli.py # typer-based CLI wrappers around service.py
__main__.py # python -m finn_eiendom → CLI entry
The five hard rules
Enforced by tests/test_architecture.py:
mcp_server.pyandcli.pyare siblings. They never import from each other. Both import only fromservice,formatting,models,config, stdlib, and their own framework (mcp/typer).service.pyis the only orchestrator. Nothing above it touches HTTP or SQLite directly.httpxlives only inhttp.py.sqlite3lives only incache.py.- Output formatting lives only in
formatting.py. Never inline in CLI or MCP tool bodies.
Development workflow — local venv
Default runtime is a project-local virtualenv. Docker is supported for packaging but optional for development.
uv venv # or: python3.12 -m venv .venv
source .venv/bin/activate
uv pip install -e ".[dev]" # or: pip install -e ".[dev]"
# from now on:
pytest
ruff check .
ruff format .
mypy finn_eiendom
finn-eiendom --help
finn-eiendom-mcp # stdio MCP server
Never install packages globally. Never add a dependency without updating pyproject.toml.
Coding rules
- Python 3.12+.
- Pydantic v2 with
model_config = ConfigDict(...). No v1class Config:blocks. - Type hints on every function signature.
- Async I/O for all network and DB code paths through
service.py. - Dependency injection for HTTP/cache clients in tests.
- Small, focused functions. One job per function. See
clean-code.instructions.md. - Errors raise with actionable messages; the MCP boundary translates them to
{"error": True, "code": ..., "message": ...}. - stdio MCP servers log to stderr only.
Code ownership — the short version
| Concern | Lives in |
|---|---|
| FINN search HTML parsing | search.py |
| FINN listing HTML parsing | ad.py |
| Norwegian number / area / URL regexes | parser.py |
| HTTP fetching + retry + delay | http.py |
| SQLite reads / writes | cache.py |
| Eiendom.no unit search/detail/comps | eiendom_no.py |
unit_vector encode/decode (msgpack) |
eiendom_no.py |
| Scoring + classification | scoring.py |
| Feedback storage | feedback.py |
| Cache-aware orchestration | service.py (get_or_fetch_*) |
| Shortlist + summary assembly | analysis.py |
| End-to-end runs | service.py (analyze_search) |
| MCP tool definitions | mcp_server.py |
| CLI command definitions | cli.py |
| Output rendering | formatting.py |
| Env-var defaults | config.py |
| Pydantic models | models.py |
Full table with "never lives in" column is in PRD.md §17.2.
Adding a feature
- Decide the home using the table above (and
PRD.md§17.2). - Implement in
service.py(oranalysis.pyif pure orchestration). - Add a service-level test.
- Add a thin MCP tool —
response_format-aware. - Add a thin CLI command —
--format-aware. - Add a renderer in
formatting.py. - Test MCP and CLI registration.
- Update PRD and instruction docs.
If the MCP tool body or CLI command body grows past ~20 lines, push logic down to service.py.
Documentation lookups — use context7
When uncertain about an external library API (FastMCP, Pydantic v2, Typer, httpx, msgpack, pytest-asyncio, respx, BeautifulSoup), call the context7 MCP server before writing code. Don't rely on training-data memory.
context7:resolve-library-id → library_id
context7:query-docs(library_id, topic) → authoritative snippets
Details in .github/instructions/docs.instructions.md.
Clean code is a hard requirement
See clean-code.instructions.md. DRY, single-responsibility, descriptive names, type hints, no dead code, comments explain why not what. If duplication slips in, the right answer is to extract it — not to copy the second instance.
Product behavior
The MVP does one thing well:
FINN search URL in
→ relevant property candidates out
→ enriched with Eiendom.no estimates
→ similar-units / comps
→ explanations
→ risks
→ next steps
→ broker questions
Always explain:
- why a property is interesting,
- price vs estimate,
- price vs comparable sales,
- renovation upside,
- hybel / rental potential,
- technical / legal risks,
- uncertainty / confidence,
- next questions for the broker.
Scores and estimates are decision support, not advice. Surface uncertainty, never hide it.