Skip to main content
Glama

carsearch — Canadian used-car search, monitoring & market-analysis platform

carsearch turns Canadian marketplace listings into a persistent, searchable, historical database and exposes it to Claude Code (or any agent) over MCP, plus a full CLI. It collects real listings from AutoTrader.ca (more providers pluggable), keeps every price/status change as history, deduplicates physical vehicles across listings, geocodes properly (real distances), extracts risk signals from descriptions, computes comparable-price statistics, scores project cars under configurable profiles, detects deals, reruns saved searches on a schedule and raises alerts.

Ask Claude things like:

  • Find me every manual BMW E90 328i under $8,000 within 800 km of Toronto, prefer private sellers, reject rust buckets, rank by project-car value.

  • What interesting project cars under $7k appeared in Ontario in the last 48 hours?

  • Is this $6,200 Audi TT actually cheap relative to similar listings we've observed?

  • Show me every price drop > 10% this week. / Cars sitting 30+ days where the seller already cut the price twice.

Status per phase, tested commands and known issues: PROJECT_STATUS.md.


Architecture (short)

providers/ (autotrader ✓, cargurus/kijiji/facebook interface+probe)  →  VehicleListing (canonical)
   → collectors/ (checkpointed ProviderRun, ingest w/ snapshots+events, raw payloads)
   → dedupe/ (VIN → weighted soft signals → vehicles + candidates)  → geo/ (geocode, haversine)
   → SQLite/PostgreSQL (SQLAlchemy)  → search/  analysis/ (description, comps, deals, market)
   → scoring/ (profiles + model knowledge JSON)  → monitoring/ (saved searches, scheduler) → notifications/
   → cli/ (carsearch …)  and  mcp/ (23 tools)

Details: docs/architecture.md, docs/providers.md, docs/schema.md.

Related MCP server: federal-spend-ai

Installation

python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"            # add ".[postgres]" for PostgreSQL
cp .env.example .env               # optional; SQLite in ./data works out of the box
carsearch db-init                  # creates/upgrades data/carsearch.db

Python ≥ 3.11 (developed on 3.14). No browser, no API keys required for AutoTrader.

Database setup

  • Default: sqlite:///./data/carsearch.db (WAL). Nothing to do.

  • PostgreSQL: CARSEARCH_DATABASE_URL=postgresql+psycopg://user:pass@host/carsearch in .env, pip install -e ".[postgres]", carsearch db-init. Schema/migrations in src/carsearch/database/ (versioned, recorded in schema_migrations).

Crawling (collecting real data)

# targeted, with detail pages for new listings (VIN, exact coordinates, drivetrain, created date)
carsearch crawl autotrader --make BMW --model "3 Series" --transmission manual --max-price 8000 --min-year 2006 --max-year 2013 --details new
# regional bulk (20 listings/page, ~1.5 s/page, no detail pages)
carsearch crawl autotrader --max-price 7000 --location Toronto --radius-km 500 --max-pages 200 --details none
# province scope (centroid + covering radius server-side, exact province locally)
carsearch crawl autotrader --province ON --transmission manual --max-price 6000 --seller-type private
# incremental refresh: stop after 3 consecutive pages with nothing new/changed
carsearch crawl autotrader --transmission manual --max-price 8000 --stop-when-seen 3
# resume an interrupted run
carsearch crawl autotrader --resume 12
# re-check listings not seen for 3 days (marks removed on 404); fetch missing detail pages; geocode backfill
carsearch refresh autotrader --older-than-days 3 --limit 200
carsearch details autotrader --limit 200
carsearch geocode --limit 60
scripts/bootstrap_crawl.sh   # example multi-band bootstrap used to seed the DB

AutoTrader caps a query at 200 pages (4000 listings) → split by price band/region for full coverage (truncated=True in the run summary tells you when).

Searching

carsearch search --make BMW --model 328i --max-price 7000 --transmission manual --location Toronto --radius-km 800 --seller-type private
carsearch search --generation E90 --keywords '"one owner" -salvage' --province ON,QC --sort price_asc
carsearch new --since 48h --province ON --max-price 7000
carsearch price-drops --min 10 --since 7d
carsearch deals --province ON --max-price 7000 --profile project_car_enthusiast
carsearch deals --location Toronto --radius-km 500 --transmission manual --max-price 7000 --since 7d \
    --profile project_car_enthusiast --sort-by project --exclude-flags salvage_title,frame_rust,doesnt_run   # "best project cars" ranking
carsearch listing 42            # full record + price history + description flags
carsearch history 42            # snapshots + change events
carsearch comps 42              # comparable-price analysis with methodology
carsearch score 42 --profile project_car_enthusiast
carsearch analyze 42            # everything above + deal signals in one JSON
carsearch market --metric manual_premium --make Subaru --model WRX
carsearch market --metric price_by_mileage --generation E90 --province ON
carsearch market --metric cheapening --days 90
carsearch status [--check-providers]  ·  carsearch runs  ·  carsearch alerts

Every command accepts --json for machine-readable output.

Monitoring / saved searches / alerts

carsearch saved seed                       # the four example searches from the brief
carsearch saved create "manual E90 328i under $8k" '{"make":"BMW","model":"3 Series","min_year":2006,"max_year":2011,"max_price":8000,"transmission":"manual"}' \
    --interval-minutes 240 --alert-rules '{"price_drop_pct":10,"target_price":6500,"min_deal_score":65}' --profile project_car_enthusiast
carsearch saved run "manual E90 328i under $8k"   # incremental crawl → new listings + alerts
carsearch schedule                          # long-running loop: runs due searches, maintenance
carsearch schedule --once                   # one pass (cron-friendly)

Alerts go to console, data/alerts.jsonl and an optional webhook (CARSEARCH_WEBHOOK_URL); new sinks = subclass notifications.alerts.AlertSink. Every alert carries reasons[] (and risks).

MCP server

carsearch serve-mcp                # stdio (default)  |  --transport streamable-http --port 8765

Tools: search_cars, search_live_marketplace, get_listing, get_vehicle, get_new_listings, get_price_history, get_listing_changes, get_comparables, compare_cars, find_deals, find_price_drops, find_long_sitting_listings, analyze_listing, score_listing, analyze_market, list_saved_searches, create_saved_search, run_saved_search, delete_saved_search, get_alerts, get_status, get_generation_codes, analyze_listing_images. All return structured JSON with pagination (limit/offset/next_offset); heavy fields (description, photos, raw) are opt-in.

Connect Claude Code

claude mcp add carsearch -- /ABSOLUTE/PATH/autotrader/.venv/bin/carsearch serve-mcp
# or in .mcp.json:
{ "mcpServers": { "carsearch": { "command": "/ABSOLUTE/PATH/autotrader/.venv/bin/carsearch", "args": ["serve-mcp"] } } }

Then: "Find me the best manual project cars under $7,000 CAD within 500 km of Toronto that appeared in the last week. Compare them against historical prices, flag major mechanical/rust risks from the listings, and rank the top 10." — Claude will typically call search_cars/get_new_listings (or search_live_marketplace for fresh data), then analyze_listing/compare_cars.

How checkpoints work

Each crawl is a provider_runs row. After every page the listings are committed and last_checkpoint.next_page is advanced; per-listing failures are logged in errors and skipped. Detail enrichment commits per listing (detail_fetched_at). A run that dies is failed/interrupted and carsearch crawl --resume <id> continues from next_page (already fetched pages come from the local http_cache). Saved-search state lives in the DB. See docs/architecture.md.

How to add a provider

Implement BaseProvider in src/carsearch/providers/<name>/, map to VehicleListing in a parser.py (keep raw), register it, add fixtures + tests, document it in docs/providers.md. Nothing else changes.

Tests

pytest -q          # 42 tests: normalization, parser fixtures, ingest/price history, dedupe,
                   # geo/search, analysis (flags/comps/scoring/deals/market), crawler checkpoints, MCP tools

No test touches the network (real payload fixtures + a fake provider).

  • SQLite is fine for hundreds of thousands of listings; switch to PostgreSQL for concurrent writers.

Available Tools

23 tools
analyze_listingB

Everything about one listing: full record, price history, description risk/positive flags with evidence, comparables, project-car score with component explanations, deal signals.

Inputs: listing_id (required), profile, include_images, include_comparables_list. Returns: object

analyze_listing_imagesA

Cache the listing's photos (sha256 + perceptual hash) and run the pluggable vision backend (checklist: rust, rockers, paint mismatch, damage, interior, warning lights, mods, tires, stance, engine bay).

Inputs: listing_id (required), force. Returns: object

analyze_marketC

Market analytics: median prices over time / by mileage / by group, manual & AWD premiums, dealer vs private, cheapening models, turnover, long-sitting.

Inputs: days, make, model, metric, +11 more. Returns: object

compare_carsA

Side-by-side comparison of several listings: key facts, comps position, project score, deal signals and description risk flags.

Inputs: listing_ids (required), profile. Returns: object

find_dealsA

Rank listings by deal signals and/or project-car score: comps position, price drops, time on market, seller, description risk flags — each item carries reasons, risks, risk_flags, project_score. Use sort_by='project' + a profile to answer 'best project cars'.

Inputs: make, limit, model, since, +15 more. Returns: object

find_long_sitting_listingsC

Active listings on the market for at least min_days (optionally with several price cuts).

Inputs: make, limit, model, offset, +7 more. Returns: object

find_price_dropsB

Listings whose asking price dropped by >= min_pct within the window (single or cumulative drops).

Inputs: make, limit, model, since, +7 more. Returns: object

get_alertsC

Recent alerts (new matches, price drops, target price, rare manuals, deal scores) with reasons.

Inputs: rule, limit, since. Returns: object

get_comparablesB

Comparable-price analysis: sample size, median/mean/quartiles, mileage-adjusted median, manual/AWD/dealer premiums, subject percentile and methodology. Pass a listing_id OR a vehicle spec.

Inputs: make, trim, year, model, +9 more. Returns: object

get_generation_codesA

Supported generation/chassis labels per make/model (for the generation filter) and scoring profiles.

Inputs: no named parameters. Returns: object

get_listingC

Full listing record with price history, change events, description flags, vehicle link and same-vehicle candidates.

Inputs: listing_id (required), include_photos. Returns: object

get_listing_changesC

Change events across the database (or for one listing) within a window.

Inputs: make, limit, model, since, +5 more. Returns: object

get_new_listingsB

Listings first observed since a given time (newest first).

Inputs: make, limit, model, since, +8 more. Returns: object

get_price_historyB

Price/mileage/status snapshots and the derived summary (days on market, drops, absolute/percent change).

Inputs: listing_id (required). Returns: object

get_statusB

Collector health: DB counts by source/province, recent runs, HTTP metrics, provider capabilities.

Inputs: check_providers. Returns: object

get_vehicleB

A physical vehicle (dedupe cluster) with all listings that were matched to it across marketplaces/time.

Inputs: vehicle_id (required). Returns: object

list_saved_searchesB

Saved (monitored) searches with schedule state.

Inputs: no named parameters. Returns: object

score_listingC

Project-car score (0-100) with per-component scores/weights/explanations under a named or custom profile.

Inputs: listing_id (required), profile. Returns: object

search_carsA

Search the local historical database of collected listings with rich filters, geography and pagination.

Inputs: make, sort, trim, limit, +30 more. Returns: object

search_live_marketplaceA

Run a fresh marketplace search (stores everything with history), then return matching listings from the local DB including ones seen earlier. Use for up-to-the-minute results.

Inputs: make, sort, limit, model, +17 more. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.5/5.0

    Scored across 23 tools

    Disambiguation5/5

    Each tool targets a distinct resource or analytical mode: search vs. live search, listing vs. vehicle vs. price history vs. changes, comparables vs. comparison vs. market analysis. Even the find_* tools are separated by clear signals (deals, price drops, long-sitting), so an agent should be able to select the right tool confidently.

    Naming Consistency5/5

    Tool names consistently follow a snake_case verb_noun pattern: search_cars, get_listing, find_deals, analyze_listing, list_saved_searches, delete_saved_search. Minor variations like search_live_marketplace or analyze_listing_images remain readable and fit the same convention.

    Tool Count3/5

    At 23 tools, the server is on the heavy side and falls into the 16-25 range that feels dense. The broad car-search/analysis domain justifies many of them, but the overall surface could be streamlined or grouped further.

    Completeness5/5

    The tool surface covers the full workflow: historical and live search, listing/vehicle detail, price history and changes, comparables, market analytics, image/listing analysis, scoring, saved-search lifecycle, and alert retrieval. Saved searches support create/update, run, list, and delete, so there are no obvious dead ends for the stated domain.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      MCP server for structured public vehicle data from StartMyCar, providing tools to list makes, models, problems, reviews, fuse box data, manuals, guides, and compare models.
      10 npm
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      MCP server for analyzing Canadian federal spending data, offering tools for contract search, NLP, semantic search, anomaly detection, and money-flow tracing.
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      MCP server for the used-Mac market, enabling AI assistants to search live listings across multiple marketplaces, get price statistics, check listing trust, lookup serial numbers, retrieve condition reports, and create email alerts.
      31 npm
      2
      MIT
    • F
      license
      A
      quality
      D
      maintenance
      MCP server for searching and monitoring RedFlagDeals deals. Provides tools to search deals, get deal details, and set up monitors for new deals.
      6
      -