PluginWorld
Fi

finn-seller-mcp

MCP

Enables Claude and other MCP clients to search finn.no and create, manage, and publish Torget ads from photos — preparing images (EXIF/GPS stripping, resize, HEIC), writing titles and Norwegian descriptions, suggesting category and market price, checking for duplicates, and publishing only after explicit confirmation. Read-only search, listing details, saved-search diffs, and management of your own ads are also supported through a locally stored logged-in session.

@ML-senior · MIT License · updated today

SECURITY

B

SCORE

60

STARS

▲ 0

PLUG IN

git clone https://github.com/ML-senior/finn-seller-mcp.git

See the README to configure this MCP server

README

finn-seller-mcp — let Claude sell your stuff on finn.no

🛒 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


✨ 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

Claude Desktop

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 Code
claude mcp add finn -- uv --directory /absolute/path/to/finn-seller-mcp run finn-seller-mcp
Any other MCP client (stdio)

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-time BankID 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
The ad payload (confirmed shape)
{
  "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

🔐 Session
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.
🏷️ Selling
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
📋 Your ads
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.
🔎 Search (from the original read-only server)
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.
📚 Code tables

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.
What gets recorded
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

not_logged_in / session expired

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

bankid_required on publish

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

finn_validation with VALID_MANAGED "Ugyldig verdi" on category

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

possible_duplicate

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

Playwright: "your OS is not officially supported"

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

Recon login hangs

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).

Made for MCP

SIMILAR PLUGINS