finn-seller-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@finn-seller-mcp sell my old iPhone on Torget from these photos"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
🛒 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.
⭐ If this saves you time, star the repo — it helps others find it.
📖 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 chromium2. Register with your MCP client
Edit claude_desktop_config.json and restart Claude Desktop:
OS | Path |
Linux |
|
macOS |
|
Windows |
|
{
"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-mcpCommand: 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.
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 |
| Opens a visible Chromium on finn.no so you log in (e-mail + one-time code). The assistant never handles credentials. |
| Checks whether the saved profile is logged in; returns how many ads the account has. |
Tool | What it does | Touches finn.no? |
| Title + photo match against local history and all your finn.no ads (active, sold, expired, drafts). | read |
| Median price of comparable "Til salgs" Torget ads; IQR outlier removal; rounded; returns example ads. | read |
| Local draft. Photos auto-rotated, EXIF/GPS stripped, resized to ≤2400 px, HEIC supported. Reports possible duplicates. | — |
| Change any field of a local draft (price, category, condition, …). | — |
| Inspect local drafts and the exact payload that would be sent. | — |
| Create/update a private draft on finn.no, upload photos, return finn.no's own validation. Never publishes. | write (private) |
| finn.no's category prediction from the uploaded photos (leaf + full path). | read |
| Unblock a draft the duplicate guard flagged — only after you say it's a different item. | — |
| Duplicate check → push → validation → with | public |
Tool | What it does |
| "Mine annonser": your ads with state, facets and the actions finn.no offers for each. |
| Run one of those actions by name (e.g. |
Tool | What it does |
| Search |
| Full listing by FINN-kode (JSON-LD parsing, 24 h cache). |
| Persist named searches. |
| 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 |
|
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 |
|
Nothing goes public without your "yes" |
|
Destructive actions are gated |
|
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 |
🧭 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/startA Chromium window opens with the same profile the server uses — log in.
Click through the flows (new ad, photos, price, "Publiser", Mine annonser, edit…). Press the red 📸 Snapshot form button on each form step.
Close the window.
File | Content |
| Endpoint map grouped by method + path template (IDs → |
| Same, machine-readable |
| Every API call: headers, bodies, status, responses — redacted (cookies, tokens, e-mails, phone numbers) |
| Large bodies; multipart uploads summarized (field names, sizes — no file bytes) |
| Form snapshots: every input/select incl. shadow DOM, labels, options, selectors + full-page screenshot |
| 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 |
|
| Persistent Chromium profile (your session) |
|
|
|
| Playwright's Chromium | Path to a custom Chromium/Chrome binary |
|
| Base for profile, drafts, cache |
|
| Search/listing cache (24 h) |
|
| Search backend ( |
|
| 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, duplicatesTests 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 toolsad_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
check_saved_searchA
Run a saved search, return only listings that are new since the last check.
Updates the saved search's ``last_checked_at`` and ``last_finnkodes``.
On the first call all current results are "new".
Inputs: name (required). Returns: result
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
delete_saved_searchA
Delete a saved search by name. Returns True if it existed.
Inputs: name (required). Returns: result
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
save_searchB
Persist a named search for repeated monitoring via check_saved_search.
Inputs: name (required), query (required), vertical (required), filters. Returns: name, query, filters, vertical, +2 more
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
Scored across 20 tools
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.
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.
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.
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
Related MCP Connectors
Hemnet MCP server for ChatGPT, Claude and any AI agent: search listings, get sold prices, list saved homes, save search and follow home value in your own Hemnet account. Cotlet works in the account through your Cloud Browser, a real Chrome where you sign in to Hemnet yourself; Cotlet does not store your password and you need no Hemnet API key. About the official Hemnet API: Hemnet has no public or developer API for buyers or owners (checked 11 Oct 2026). Its own AI access is the Hemnet app inside ChatGPT, which runs only in ChatGPT and gives your agent or scripts no way to read your saved homes or saved searches. Setup and FAQ: https://cotlet.dev/integrations/hemnet-api-mcp-server
Create, edit, relist and take down Czech and Slovak classified ads from your AI agent.
Hosted Google Ads MCP for Claude and ChatGPT. Preview every write. No developer token.
Analyze, edit and launch Meta (Facebook and Instagram) ads from Claude, ChatGPT, Cursor or Codex. Query insights and campaign health, change budgets, bids, status and targeting with undo, bulk-launch many ads across campaigns and ad sets from a live ad's settings, and duplicate by post to keep social proof. Choose per ad account what the agent may do and what waits in the Agent Inbox for approval. Requires a paid Ads Uploader account with MCP access.
Related MCP Servers
- AlicenseAqualityDmaintenanceAn 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.620 PyPI1MIT
- FlicenseAqualityCmaintenanceEnables searching Facebook Marketplace listings from Claude using your existing Facebook session without a browser.72-
- AlicenseAqualityCmaintenanceEnables posting second-hand items to Facebook Marketplace from local photo folders via Claude, featuring login, listing creation, pricing reference, and publishing tools.76MIT
- AlicenseAqualityBmaintenanceEnables 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.23MIT