vinted-seller-mcp
# vinted-seller-mcp
**Photos in, a ready-to-publish Vinted listing out.** An MCP server and CLI that researches comparable listings, estimates a defensible price, finds the right category and brand, writes the listing, fills Vinted's real form — and stops, until you say publish.
Works with [Hermes](#hermes), [Claude Desktop, Claude Code](integrations/claude/README.md), and any MCP client. One stdio server behind all of them.
```
photos + what you can see
↓
search_similar_items → real comparables, filtered
↓
estimate_price → median, range, confidence
↓
find_category / find_brand
↓
prepare_listing → a local draft; nothing sent to Vinted
↓
⏸ you review it
↓
validate_listing → fills the real form, STOPS before publishing
↓
⏸ you approve
↓
publish_listing
```
---
## Install
```bash
git clone https://github.com/<owner>/vinted-seller-mcp
cd vinted-seller-mcp
npm install
npm run build
./scripts/install-hermes.sh
vinted-seller login --country fr
vinted-seller status --country fr
```
Requires **Node.js 20+**. `npm install` pulls Playwright's Chromium (~150 MB) on first run.
For the `vinted-seller` command to be on your PATH, either `npm link` in the checkout or `npm install -g .`. Otherwise call `node dist/cli/index.js` — the install script prints the absolute path.
---
## Signing in
```bash
vinted-seller login --country fr
```
A real Chromium window opens on Vinted. **You** sign in — password, CAPTCHA, Google login, 2FA, all of it. The command notices when you are in and closes the window.
This project never asks for, stores or handles your password, and never tries to work around a CAPTCHA or 2FA. Your session lives in a Chromium profile at `~/.vinted-seller-mcp/profile-fr/`, and Chromium keeps Vinted's own tokens fresh from there.
```bash
vinted-seller status --country fr
```
```text
Country: fr
Profile: OK
Session: authenticated
Username: yourname
User ID: 123456
```
If it lapses:
```text
Country: fr
Profile: OK
Session: expired
Session expired. Reauthentication required: `vinted-seller login --country fr`.
```
---
## Hermes
Two independent layers, so Hermes never depends on anything proprietary:
**1. The MCP server** — the actual tools, over stdio:
```bash
hermes mcp add vinted-seller --command node --args /ABSOLUTE/PATH/vinted-seller-mcp/dist/mcp/stdio.js
hermes mcp list
hermes mcp test vinted-seller
```
**2. The skill** — [`integrations/hermes/vinted-seller/SKILL.md`](integrations/hermes/vinted-seller/SKILL.md), which teaches Hermes how to *use* those tools well: identify honestly, price with evidence, never publish without asking.
`./scripts/install-hermes.sh` does both. It is idempotent — re-run it after every `git pull`, or use `./scripts/update-hermes.sh`, which pulls, rebuilds and re-registers in one step. `./scripts/uninstall-hermes.sh` removes both (add `--purge-data` to delete your session and drafts too).
If the `hermes` CLI is not on your PATH, the script builds everything anyway and prints the exact registration command and JSON config to use by hand.
### On another machine
```bash
git clone https://github.com/<owner>/vinted-seller-mcp
cd vinted-seller-mcp
./scripts/install-hermes.sh
vinted-seller login --country fr
```
Then ask Hermes: *"Here are photos of a jacket — prepare me a Vinted listing."*
---
## The tools
27 tools, each with a Zod schema, documented parameters and structured errors.
| | |
|---|---|
| **Session** | `whoami` · `session_status` |
| **Research** | `search_items` · `get_item` · `search_similar_items` · `get_similar_items` · `estimate_price` |
| **Taxonomy** | `find_category` · `find_brand` |
| **Drafting** | `prepare_listing` · `get_listing_draft` · `list_drafts` · `update_listing_draft` · `add_draft_photos` · `delete_draft` |
| **Publishing** | `validate_listing` · `publish_listing` |
| **Shop** | `my_listings` · `seller_stats` · `edit_listing` · `update_price` · `deactivate_listing` · `reactivate_listing` · `delete_listing` |
| **Messages** | `conversations` · `read_messages` · `send_message` |
`vinted-seller tools` lists them. `npm run mcp:inspect` runs a real MCP handshake and prints what a client discovers.
### The three that need permission
`publish_listing`, `delete_listing` and `send_message` all require `confirm: true` and are annotated `destructiveHint`. Without it they fail with `CONFIRMATION_REQUIRED` and a hint telling the assistant to ask you first. This is enforced in the tool schema, not just the prompt — a model that has drifted still cannot publish by accident.
`validate_listing` fills Vinted's real form completely and **never clicks publish**. That is the safe end-to-end check.
---
## The CLI
```bash
vinted-seller login --country fr # sign in (opens a real browser)
vinted-seller status --country fr # is the session alive?
vinted-seller whoami --country fr
vinted-seller search "nike air max 90" # catalogue search
vinted-seller similar --brand Nike --model "Air Max 90" --size 42
vinted-seller price --brand Nike --model "Air Max 90" --condition very_good
vinted-seller category "baskets homme"
vinted-seller brand "Nike"
vinted-seller prepare --brand Nike --model "Air Max 90" --size 42 \
--color noir --condition very_good \
--defect "usure sur le talon droit" \
--photo ./1.jpg --photo ./2.jpg --photo ./3.jpg
vinted-seller drafts # list local drafts
vinted-seller draft <draftId>
vinted-seller validate <draftId> --screenshot # fill the form, stop before publishing
vinted-seller mcp # MCP server on stdio
vinted-seller serve-http --port 8787 # MCP over Streamable HTTP
```
`--json` on any command for machine-readable output.
---
## Pricing
`estimate_price` returns the full distribution, three price points, and a confidence level:
```json
{
"count": 12, "currency": "EUR",
"minimum": 45, "lowerPercentile": 55, "median": 62,
"average": 63.5, "upperPercentile": 70, "maximum": 85,
"quickSalePrice": 53, "recommendedPrice": 62, "optimisticPrice": 72,
"confidence": "high",
"rationale": "Based on 12 comparable listings on Vinted. 2 outliers (1 EUR, 900 EUR) were excluded."
}
```
**It refuses to look precise when it isn't.** Below three usable comparables the confidence is `insufficient_data` and the rationale says so — a median of two listings is not a market price.
Comparables are found by trying several phrasings (Vinted's search is literal), then filtering out duplicates, accessories, broken/for-parts listings, mismatched storage capacities and wrong sizes. Every exclusion comes back in `rejected` with its reason, so a price can be defended.
---
## Drafts
```text
~/.vinted-seller-mcp/
├── profile-fr/ # your Chromium profile — never leaves this machine
└── drafts/
└── 01M05Z76YAAF8G9F64RHQ07M1D/
├── listing.json # fields, comparables, estimate, validation, history
└── photos/
├── 01.jpg
├── 02.jpg
└── 03.jpg
```
Statuses: `draft` → `validated` → `published`, or `failed`.
Photos are **copied** into the draft — your originals are never moved, modified or deleted. Plain files, so you can read, diff, back up or hand-edit any of it.
---
## Configuration
Everything is optional; see [`.env.example`](.env.example). There are no credentials in it.
| Variable | Default | |
|---|---|---|
| `VINTED_SELLER_HOME` | `~/.vinted-seller-mcp` | profiles, drafts, photos |
| `VINTED_COUNTRY` | `fr` | default storefront |
| `VINTED_RATE_LIMIT_PER_SEC` | `2` | per country |
| `VINTED_CACHE_TTL_MS` | `60000` | GET cache; 0 disables |
| `VINTED_HEADLESS` | `true` | `login` always shows a window |
| `VINTED_LOG_LEVEL` | `info` | JSON logs on stderr |
| `VINTED_MCP_TOKEN` | — | bearer token for `serve-http` |
23 storefronts: `fr be de at es it nl pt lu ie fi gr sk lt hr uk pl cz hu ro se dk us`.
---
## Development
```bash
npm run lint
npm run typecheck
npm run build
npm test # 124 tests
npm run check # all four
```
The browser suite drives the real Playwright code against a local replica of Vinted's listing form ([`tests/fixtures/listing-form.html`](tests/fixtures/listing-form.html)) that reproduces what breaks naive automation: a hidden file input, custom dropdowns with a search box, and the brand/size/condition/colour block that only mounts after a category is chosen. The MCP suite speaks the wire protocol to the built server exactly as Hermes does.
Tests never touch the real Vinted. Live tests are opt-in with `VINTED_LIVE_TESTS=1` and never publish, delete or send a message.
See [`docs/DECISIONS.md`](docs/DECISIONS.md) for why the project is shaped this way, [`docs/REFERENCES.md`](docs/REFERENCES.md) for what was learned from prior work and under which licences, and [`WORKLOG.md`](WORKLOG.md) for current state and known limitations.
---
## Security and good behaviour
- Your Vinted password is never requested, stored or handled. Sign-in is manual, in a window you control.
- CAPTCHAs and 2FA are never circumvented.
- Cookies, tokens and session material are redacted from logs and never returned by a tool.
- Nothing is published, deleted or sent to another person without an explicit confirmation for that specific action.
- Requests are rate-limited to 2/second per storefront by default.
- `.gitignore` excludes browser profiles, drafts, photos, `.env` and databases.
This project drives the same endpoints and the same web form your own browser uses, on your own account. Vinted publishes no public API; using this is subject to their Terms of Service and that call is yours.
---
## Publishing this to its own repository
This tree currently lives on a branch of another repository. To give it the standalone
`vinted-seller-mcp` repo it is named for, from a machine with an authenticated `gh`:
```bash
gh auth status # confirm you are signed in
gh repo create vinted-seller-mcp --private --source=. --remote=vinted --push
```
Or without `gh`: create an empty private repository on GitHub, then
```bash
git remote add vinted git@github.com:<owner>/vinted-seller-mcp.git
git push -u vinted HEAD:main
```
Nothing sensitive is tracked — see [Security](#security-and-good-behaviour) — so the
history is safe to move as is.
---
## Licence
[MIT](LICENSE).
TDQS
Scored across 27 tools
Most tools have clearly distinct purposes, with the three search-related tools (search_items, search_similar_items, get_similar_items) differentiated by their descriptions. Similarly, update_price vs edit_listing are separated by scope, and deactivate vs delete are unambiguous. Only minor overlap exists between session-related tools whoami and session_status.
The majority of tools follow a verb_noun pattern (search_items, get_item, update_price, publish_listing), but several deviate with noun-only names like whoami, my_listings, seller_stats, conversations, and session_status. This mix of verb-led and noun-only names creates a slight inconsistency in the naming scheme.
At 27 tools, the count exceeds the 25-tool threshold considered 'too many' in the calibration. While the breadth of functionality for a seller MCP is extensive, the large number feels heavy, and some tools (e.g., multiple search variants) could potentially be consolidated.
The tool set covers the full selling lifecycle: research (search, estimate_price, find_brand, find_category), draft management (prepare, update, add photos, validate), publishing, and post-publish actions (edit, price, deactivate, delete). Messaging and session handling are also included. No obvious gaps exist for the stated domain.