Skip to main content
Glama

🛒 finn-seller-mcp

An MCP server that lets Claude (or any MCP client) search and sell on finn.no — Norway's largest marketplace. Send a photo, get a finished Torget ad: description, category, market price, duplicate check, publish.

Python MCP Playwright Tests License: MIT Tools

⭐ If this saves you time, star the repo — it helps others find it.

English · Русский


🚀 Quick Start

🧰 Tools

🔄 How it works

🛡️ Safety

🧭 Recon

⚙️ Config

🩺 Troubleshooting

❓ FAQ


📖 Table of contents


Related MCP server: Facebook Marketplace MCP

✨ What it does

🏷️ Sell

  • 📸 Photo prep — auto-rotate, strip EXIF/GPS, resize, HEIC (iPhone) support

  • ✍️ Ad writing — the assistant writes title + Norwegian description from your photos

  • 🗂️ Category from photos — uses finn.no's own category prediction

  • 💰 Market price — median of comparable ads, outliers removed

  • 🔁 Duplicate guard — won't re-list something you already listed or sold

  • 🚦 Two-step publish — private draft on finn.no → preview → your "yes" → live

🔎 Search & manage

  • 🔍 Search Torget, real estate, rentals, used cars, jobs

  • 📄 Full listing details by FINN-kode

  • 🔔 Saved searches with "what's new since last time"

  • 📋 Mine annonser — list your ads with state

  • ⚡ Ad actions — run the actions finn.no offers per ad (delete, …), with confirmation

  • 🔐 Passwordless login kept in a local browser profile


🆚 Why this project

Existing finn.no MCP servers are read-only scrapers. This one adds the whole seller side, built on finn.no's internal JSON API (mapped with an included recon tool) rather than brittle form clicking.

Read-only scrapers

finn-seller-mcp

Search & listing details

✅

✅

Saved searches with diff

some

✅

Log in to your account

❌

✅ (you log in, never the AI)

Create / publish Torget ads

❌

✅

Photo upload + EXIF/GPS strip

❌

✅

Category suggestion from photos

❌

✅

Market-price suggestion

❌

✅

Duplicate protection (local + your finn.no ads)

❌

✅

Manage "Mine annonser"

❌

✅

Recon tool to re-map the site when it changes

❌

✅


🚀 Quick Start

1. Install (needs uv and Python 3.11+)

git clone https://github.com/ml-senior/finn-seller-mcp
cd finn-seller-mcp
uv sync
uv run playwright install chromium

2. Register with your MCP client

Edit claude_desktop_config.json and restart Claude Desktop:

OS

Path

Linux

~/.config/Claude/claude_desktop_config.json

macOS

~/Library/Application Support/Claude/claude_desktop_config.json

Windows

%APPDATA%\Claude\claude_desktop_config.json

{
  "mcpServers": {
    "finn": {
      "command": "uv",
      "args": ["--directory", "/absolute/path/to/finn-seller-mcp", "run", "finn-seller-mcp"]
    }
  }
}
claude mcp add finn -- uv --directory /absolute/path/to/finn-seller-mcp run finn-seller-mcp

Command: uv, args: --directory /absolute/path/to/finn-seller-mcp run finn-seller-mcp. The server speaks MCP over stdio.

3. Log in once

Ask the assistant to run login (or run the recon script, below). A Chromium window opens on finn.no — you type your e-mail and the one-time code from Vend. The session is stored in a local browser profile and reused from then on.

4. Sell something

"Sell this controller" + 📸 photo

That's it. The assistant checks for duplicates, prepares a private draft on finn.no, shows you the preview and publishes only after you say yes.

IMPORTANT

finn.no asks for a one-timeBankID identity verification before your first Torget ad goes live. If publish_draft returns bankid_required, open the edit_url it gives you, press Gå videre → Verifiser med BankID, finish it yourself, then ask the assistant to publish again.


💬 Example session

You:     Sell this. [photo of a PS5 controller]
Claude:  ▸ check_duplicate      → nothing similar on your account
         ▸ suggest_price        → median 500 kr from 14 comparable ads
         ▸ create_draft         → photo cleaned (EXIF/GPS removed)
         ▸ push_draft           → private draft created on finn.no
         ▸ suggest_category     → Elektronikk › Spill og konsoll › Tilbehør
         Here's the ad:
           PS5 DualSense kontroller – hvit · Pent brukt · 500 kr · 1 photo
           "Original Sony DualSense trådløs kontroller …"
         Publish?
You:     yes
Claude:  ▸ publish_draft(confirm=true) → live ✅

🔄 How it works

flowchart LR
    U([You + photos]) --> A[Assistant]
    A -->|check_duplicate| D{Already listed?}
    D -- yes --> S[Stop and show matches]
    D -- no --> P[suggest_price<br/>median of comparables]
    P --> C[create_draft<br/>EXIF/GPS strip, resize]
    C --> PU[push_draft<br/>private draft on finn.no]
    PU --> SC[suggest_category<br/>finn.no prediction]
    SC --> PV[Preview to you]
    PV -->|your explicit yes| PB[publish_draft confirm=true]
    PB --> L([Live on finn.no])

Under the hood the server talks to the same JSON endpoints the finn.no web form uses, through a persistent Playwright Chromium profile so requests carry your logged-in session:

sequenceDiagram
    participant M as finn-seller-mcp
    participant F as finn.no
    M->>F: POST create-item … /api/create (adType=recommerce)
    F-->>M: 301 → /recommerce/create/{adId}
    loop each photo
        M->>F: POST /recommerce/create/api/image/{adId} (multipart "image")
        F-->>M: { path: "item/{adId}/{uuid}", width, height }
    end
    M->>F: PUT /recommerce/create/api/item/{adId}  e-tag: W/"rev"  { commit:false, data }
    F-->>M: { state:"draft", violations:[], etag }
    M->>F: POST /recommerce/create/api/predictions/categories/{adId}
    Note over M: user confirms in chat
    M->>F: PUT /recommerce/create/api/item/{adId}  { commit:true, data }
    F-->>M: published — or { requiredVerification:true } → BankID needed
{
  "trade_type": "SELL",
  "category": 65,
  "condition": 3,
  "title": "PS5 DualSense kontroller – hvit",
  "description": "Original Sony DualSense …",
  "price": { "price_amount": 500 },
  "address": { "postalcode": "0150", "country": "NO" },
  "image": [ { "uri": "item/{adId}/{uuid}", "width": 960, "height": 1280, "description": "optional caption" } ]
}

Saves use optimistic locking: every PUT sends the last etag in an e-tag request header. No CSRF token is involved — only the session cookies plus Origin/Referer. Full notes: RECON.md.


🧰 Tool reference

Tool

What it does

login

Opens a visible Chromium on finn.no so you log in (e-mail + one-time code). The assistant never handles credentials.

login_status

Checks whether the saved profile is logged in; returns how many ads the account has.

Tool

What it does

Touches finn.no?

check_duplicate

Title + photo match against local history and all your finn.no ads (active, sold, expired, drafts).

read

suggest_price

Median price of comparable "Til salgs" Torget ads; IQR outlier removal; rounded; returns example ads.

read

create_draft

Local draft. Photos auto-rotated, EXIF/GPS stripped, resized to ≤2400 px, HEIC supported. Reports possible duplicates.

—

update_draft

Change any field of a local draft (price, category, condition, …).

—

list_drafts / preview_draft

Inspect local drafts and the exact payload that would be sent.

—

push_draft

Create/update a private draft on finn.no, upload photos, return finn.no's own validation. Never publishes.

write (private)

suggest_category

finn.no's category prediction from the uploaded photos (leaf + full path).

read

confirm_not_duplicate

Unblock a draft the duplicate guard flagged — only after you say it's a different item.

—

publish_draft

Duplicate check → push → validation → with confirm=true only: publish. Detects the BankID requirement.

public

Tool

What it does

my_ads

"Mine annonser": your ads with state, facets and the actions finn.no offers for each.

ad_action

Run one of those actions by name (e.g. DELETE). Anything that changes an ad needs confirm=true.

Tool

What it does

search_finn

Search bap (Torget), homes, lettings, cars_used, jobs with free text + finn.no URL filters.

get_listing

Full listing by FINN-kode (JSON-LD parsing, 24 h cache).

save_search / list_saved_searches / delete_saved_search

Persist named searches.

check_saved_search

Run a saved search and return only listings new since the last check.

Condition — 1 Helt ny · 2 Som ny · 3 Pent brukt · 4 Godt brukt · 5 Må fikses

Top-level Torget categories — 76 Antikviteter og kunst · 77 Dyr og utstyr · 93 Elektronikk og hvitevarer · 68 Foreldre og barn · 86 Fritid, hobby og underholdning · 67 Hage, oppussing og hus · 71 Klær, kosmetikk og tilbehør · 78 Møbler og interiør · 91 Næringsvirksomhet · 69 Sport og friluftsliv · 90 Utstyr til bil, båt og MC

Ads need a leaf category id (e.g. 93 › 3905 › 65 → use 65). Easiest: push_draft with a best guess, then suggest_category.


💰 Pricing policy

Situation

Price used

You name a price

Exactly your price

You don't

suggest_price → median of comparable "Til salgs" ads, IQR outliers dropped, rounded (10 / 50 / 100 / 500 kr steps by size)

Fewer than 3 comparables

The assistant asks you instead of guessing

The policy is written into the tool descriptions, so any MCP client follows it.


🔁 Duplicate protection

Before creating and again before publishing, every new ad is compared with:

  • 🗃️ Local history — every draft/ad made through this server, photos matched by a 64-bit perceptual dHash (survives resizing, re-compression and EXIF stripping; ≤10-bit distance = same picture).

  • 🌐 Your finn.no account — all ads in "Mine annonser" in any state, matched by normalized title similarity (≥85 %, or ≥60 % plus a shared photo).

A hit blocks publish_draft. Only confirm_not_duplicate, called after you say "it's a different item", unblocks it.


🛡️ Safety model

Rule

How it's enforced

The AI never logs in for you

login opens a window; you type e-mail and code yourself.

Nothing goes public without your "yes"

publish_draft returns a preview unless confirm=true; tool docs require explicit user consent.

Destructive actions are gated

ad_action requires confirm=true for any non-GET action (delete, …).

No accidental re-listing

Duplicate guard blocks publishing.

Your photos don't leak location

EXIF (incl. GPS) is dropped; images re-encoded.

Identity checks stay yours

BankID is detected and handed back to you, never automated.

Recon can't publish by accident

The recon script aborts any request containing "commit": true and payment-like URLs.


🧭 Recon: mapping the site yourself

finn.no can change its internals at any time. scripts/recon.py lets anyone re-map the seller flow in minutes without publishing anything:

uv run python scripts/recon.py --start https://www.finn.no/create-item/start
  1. A Chromium window opens with the same profile the server uses — log in.

  2. Click through the flows (new ad, photos, price, "Publiser", Mine annonser, edit…). Press the red 📸 Snapshot form button on each form step.

  3. Close the window.

File

Content

recon/<ts>/endpoints.md

Endpoint map grouped by method + path template (IDs → {id}), write calls first, ⛔ = blocked

recon/<ts>/endpoints.json

Same, machine-readable

recon/<ts>/requests.jsonl

Every API call: headers, bodies, status, responses — redacted (cookies, tokens, e-mails, phone numbers)

recon/<ts>/bodies/

Large bodies; multipart uploads summarized (field names, sizes — no file bytes)

recon/<ts>/forms/

Form snapshots: every input/select incl. shadow DOM, labels, options, selectors + full-page screenshot

recon/<ts>/private/full.har

Full HAR with cookies — never share

Safety flags: --block REGEX adds patterns; --no-default-block disables blocking (real actions will happen!). Tracker hosts are filtered out. Non-UTF-8 request bodies are handled safely.


⚙️ Configuration

Variable

Default

Purpose

FINN_PROFILE_DIR

$XDG_DATA_HOME/finn-mcp/browser-profile

Persistent Chromium profile (your session)

FINN_HEADLESS

1

0 shows the browser for all calls (debugging)

FINN_CHROMIUM

Playwright's Chromium

Path to a custom Chromium/Chrome binary

XDG_DATA_HOME

~/.local/share

Base for profile, drafts, cache

FINN_CACHE_DB

…/finn-mcp/cache.sqlite

Search/listing cache (24 h)

FINN_BACKEND

scraper

Search backend (official is a stub)

FINN_BASE_URL

https://www.finn.no

Override for tests against a mock server only

Local data lives in ~/.local/share/finn-mcp/: browser-profile/, drafts/<id>/draft.json + cleaned photos, cache.sqlite.


🗂️ Project layout

finn-seller-mcp/
├── src/finn_mcp/
│   ├── server.py            # FastMCP server, search tools, registers seller tools
│   ├── scraper/             # read-only HTML/JSON-LD scrapers (bap, homes, cars, jobs)
│   ├── backend/             # search backend abstraction
│   └── seller/
│       ├── tools.py         # seller MCP tools
│       ├── api.py           # finn.no seller JSON API client (via Playwright context)
│       ├── browser.py       # persistent Chromium session, interactive login
│       ├── drafts.py        # local draft store + validation
│       ├── photos.py        # EXIF/GPS strip, rotate, resize, HEIC
│       ├── dupes.py         # perceptual-hash + title duplicate detection
│       ├── pricing.py       # market-price median with outlier removal
│       └── selectors.py     # every site-specific URL/selector in one place
├── scripts/recon.py         # site recorder with publish blocking
├── tests/                   # fixtures-based tests (no network)
├── RECON.md                 # what we know about finn.no internals
└── docs/                    # translations, assets

🧪 Development & tests

uv sync
uv run pytest -q          # offline: HTML fixtures, pricing, duplicates

Tests never hit finn.no. For end-to-end work, point FINN_BASE_URL at a mock server that replays shapes from RECON.md.


🩺 Troubleshooting

Run the login tool (or the recon script) and log in again. Sessions are long-lived but finn.no expires them eventually.

One-time identity check by finn.no. Open the returned edit_url, press Gå videre → Verifiser med BankID, complete it, then publish again.

You used a parent category. Use a leaf id — run suggest_category after push_draft.

Working as intended. Check the matches; if it really is a different item, tell the assistant so it calls confirm_not_duplicate.

Harmless on Kali/Arch/etc. — Playwright downloads an Ubuntu build that works. Or set FINN_CHROMIUM to your own Chromium.

Update to the latest scripts/recon.py — older versions crashed on non-UTF-8 request bodies sent by the login page, which stalled the request.


❓ FAQ

Is there an official finn.no API for this? Not for private sellers. FINN's import APIs are B2B (jobs, real estate, dealers) and require a contract. This project uses the same internal endpoints as the finn.no website, with your own session.

Does the AI see my password? There is no password — login is e-mail + one-time code, and you type both yourself in a real browser window.

Can it publish without asking? No. publish_draft without confirm=true only returns a preview, and the tool contract requires your explicit "yes".

Which categories work? All Torget leaf categories. Category-specific extra fields can be passed via attributes.

Does it work for cars / real estate? Search: yes. Selling: Torget only for now.


🗺️ Roadmap

  • Read-only search, listings, saved searches

  • Login via persistent browser profile

  • Create / push / publish Torget ads, photo upload

  • Category prediction, market price, duplicate guard

  • Mine annonser + generic ad actions

  • Recon tool with publish blocking

  • "Gi bort" (give away) trade type

  • Fiks ferdig (shipping) fields

  • Mark sold / deactivate / renew on live ads (action names pending recon)

  • Buyer messages

  • Full category tree export

  • Price from Klarna Prisguiden (new-price ceiling)


🤝 Contributing

PRs welcome — see CONTRIBUTING.md. If finn.no changed something, run the recon script and attach the redacted endpoints.md (never the private/ folder).


⚖️ Disclaimer

Independent open-source project, not affiliated with or endorsed by FINN.no / Vend Marketplaces. It automates your own account through finn.no's web endpoints; you are responsible for complying with finn.no's terms of use. Keep usage personal and moderate — no bulk listing, no scraping at scale.


📄 License & credits

MIT © 2026 ml-senior

Search/scraper layer based on aHk-coder/finn-mcp (MIT). Condition and category code tables adapted from viktorfa/finn_mcp (MIT).

Available Tools

20 tools
ad_actionA

Run an action finn.no offers for one of the user's ads (names come from my_ads, e.g. DELETE).

    Anything that changes the ad (DELETE, mark sold, deactivate ...) needs
    confirm=true, given only after the user explicitly agreed in chat.
    DELETE is permanent.
    

Inputs: action (required), finnkode (required), confirm. Returns: object

check_duplicateA

Check whether an item was already listed (locally or on the user's finn.no account).

    Call this BEFORE preparing a new ad. Local photos are matched by
    perceptual hash; finn.no ads (any state: active, sold, expired, draft)
    are matched by title.
    

Inputs: title (required), photo_paths. Returns: object

confirm_not_duplicateA

Mark a draft as a genuinely different item so publish_draft stops blocking it.

    Only call after the USER said in chat that it is not the same item.
    

Inputs: draft_id (required). Returns: object

create_draftA

Create a LOCAL draft ad (nothing is sent to finn.no yet).

    Photos are auto-rotated, stripped of EXIF/GPS and resized; the first
    photo is the cover. category = numeric LEAF id (e.g. "43" Bærbar PC;
    use suggest_category after push_draft if unsure). condition: 1=Helt ny
    2=Som ny 3=Pent brukt 4=Godt brukt 5=Må fikses. trade_type: 1=Til salgs.
    Price is in whole NOK.

    PRICE POLICY: if the user named a price, use exactly that. Otherwise call
    suggest_price first and use its `suggested_price` (market median), and
    tell the user which comparables it is based on.
    

Inputs: title (required), description (required), photo_paths (required), price, +6 more. Returns: object

get_listingA

Fetch a full listing by its finnkode.

If ``vertical`` is omitted the server tries a cache hint first, then
probes each vertical until one returns a valid page. Pages are cached
for 24 hours in a local SQLite database.

``description`` is plain text and ``images`` has one URL per photo.
``attributes`` carries the spec list of the page (cars: year, mileage,
fuel, transmission, fees ...; real estate: areas, costs, facilities ...).

Inputs: finnkode (required), vertical. Returns: result

list_draftsA

List local drafts.

    `status` is checked against finn.no ("Mine annonser") when logged in:
    `remote_state` is finn.no's own state (DRAFT, ACTIVE ...), "missing" if
    the ad no longer exists there, or absent if it could not be checked.
    

Inputs: no named parameters. Returns: result

list_saved_searchesA

Return every saved search in the local store, ordered by name.

Inputs: no named parameters. Returns: result

loginB

Open a visible browser window on finn.no so the USER can log in by hand.

Login is passwordless (e-mail + one-time code from login.vend.no). The assistant never types credentials. The session is kept in the local profile.

Inputs: timeout_seconds. Returns: object

login_statusA

Check whether the saved browser profile is logged in to finn.no.

Inputs: no named parameters. Returns: object

my_adsC

List the user's ads on finn.no ("Mine annonser") with state and available actions.

    facet: ALL or DRAFT (confirmed); the response's `facets` lists the others.
    

Inputs: facet, limit, offset. Returns: object

preview_draftA

Show a draft exactly as it would be sent to finn.no, plus blocking problems.

Inputs: draft_id (required). Returns: object

publish_draftA

Publish a draft on finn.no. PUBLIC.

    Flow: duplicate check (local + finn.no account) -> push as private draft
    -> finn.no validation -> only with confirm=true: publish.
    Call with confirm=true ONLY after the user saw the preview and said yes in chat.
    

Inputs: draft_id (required), confirm. Returns: object

push_draftA

Upload a local draft to finn.no as a PRIVATE draft (not published, not visible).

    Creates the finn.no draft if needed, uploads photos, saves the fields and
    returns finn.no's own validation (`violations`). Safe to call repeatedly.
    

Inputs: draft_id (required). Returns: object

search_finnA

Search finn.no within a vertical.

``vertical`` is one of: ``bap`` (Torget / used goods), ``homes`` (real
estate for sale), ``lettings`` (real estate for rent), ``cars_used``,
``cars_new`` (not yet supported by the scraper backend), ``jobs``.

``query`` is a free-text keyword. ``filters`` is a dict of extra
URL-query parameters understood by finn.no (e.g. ``{"price_to":"300000"}``
for cars, ``{"location":"1.20001.20061"}`` for Oslo).

Returns up to ~50 ``SearchResult`` entries. Fewer if finn.no returned
fewer matches; an empty list is a valid result.

``location`` is the place shown on the card (street address for real
estate, town for Torget, cars and jobs). ``extra`` holds per-vertical
fields: ``area_m2``/``bedrooms``/``property_type``/``total_price_nok`` for
real estate, ``year``/``mileage_km``/``fuel``/``transmission`` for cars,
``employer``/``published_at`` for jobs. For cars ``price`` is finn.no's
"Totalpris" (asking price plus the re-registration fee); ``get_listing``
gives the split.

Inputs: query (required), vertical (required), page, filters. Returns: result

suggest_categoryA

finn.no's category suggestions based on the draft's uploaded photos (needs push_draft first).

Inputs: draft_id (required). Returns: object

suggest_priceA

Market price for an item: median of comparable "Til salgs" ads on finn.no Torget.

    Default price source whenever the user did not name a price. `query` =
    short search words (brand + model, e.g. "DualSense PS5 kontroller").
    Outliers are dropped (IQR); result is rounded. Show the user the
    suggested price and a few of the `examples`.
    

Inputs: query (required), condition. Returns: object

update_draftC

Change fields of a local draft (e.g. {"price": 1500, "category": "43"}).

Inputs: changes (required), draft_id (required). Returns: object

Tool Schema Changelog

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

Loading tool schema changelog…

    TDQS

    A3.6/5.0

    Scored across 20 tools

    Disambiguation4/5

    Most tools map to a clearly unique resource or action, including separate search, draft, remote-ad, and saved-search tools. The main possible confusion is within the deliberate draft pipeline (preview_draft, push_draft, publish_draft) and local-vs-remote listing (list_drafts vs my_ads), but descriptions make those boundaries explicit.

    Naming Consistency4/5

    The set consistently uses snake_case and is dominated by verb_noun names such as save_search, create_draft, and publish_draft. A few exceptions like ad_action, login_status, login, and my_ads are minor deviations but remain readable and predictable.

    Tool Count4/5

    At 20 tools, the surface is slightly above the typical 3-15 range for a single MCP server. However, the seller workflow legitimately spans search, saved searches, local drafts, publishing, duplicate checks, pricing, category advice, login, and remote ad management, so most tools earn their place.

    Completeness4/5

    The tool set covers the core seller lifecycle: discovery, local drafting, validation, duplicate checking, pricing/category assistance, publishing, saved searches, and managing existing finn.no ads. Minor gaps exist, such as editing an already-published ad outside ad_action or logout/messaging support, but they are not central to the stated purpose.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      D
      maintenance
      An MCP server that provides access to finn.no, Norway's largest online classifieds marketplace, enabling searches and listing retrieval across used goods, real estate, cars, and jobs.
      6
      20 PyPI
      1
      MIT
    • A
      license
      A
      quality
      B
      maintenance
      Enables turning photos and observed facts into a ready-to-publish Leboncoin ad, with comparable search, asking-price statistics, category lookup, local drafts, and browser form automation that stops one click short of publishing until approved.
      23
      MIT