intermarche-drive-mcp
v0.1.0
Published
MCP server that drives Intermarché Drive: search, cart, slots, prepare checkout (one click short of placing the order).
Maintainers
Readme
Intermarché Drive MCP Server
An MCP (Model Context Protocol) server that lets an LLM agent do your grocery shopping on Intermarché Drive end-to-end: search products, build a cart, pick a collection slot, and place the order.
The goal is: say "order this week's groceries: milk, eggs, chicken, the usual veggies, and pick it up Saturday morning" to Claude/Cursor/your agent of choice, and have it happen.
Installation
There are two ways to run the server: from source (recommended for development and the first install) or via Docker Compose (recommended for keeping it running 24/7). Either way you need a Chromium-based browser logged in to intermarche.com — the server never sees your password; cookies are pushed by a tiny browser extension.
Prerequisites
| | Source install | Docker install | |---|---|---| | Node 22+ | yes | not needed | | Docker + Compose | not needed | yes | | Chrome / Edge / Brave (logged in to intermarche.com) | yes | yes | | Your 5-digit Intermarché store code | yes | yes |
To find your store code: log in to intermarche.com, set your active store from
the header, then look at the URL on any product page — pdvRef=NNNNN or the
last segment of a product URL is the code. (Example: 09571 for Hyper Toulouse
Lardenne, 11131 for Super Ramonville Saint-Agne.)
Option A — Run from npm (no clone, no build)
Once published, the whole server runs from npx — no need to clone or check
out the repo. Skip straight to Step 3 with your MCP client:
# Claude Code
claude mcp add intermarche-drive \
--env STORE_ID=09571 \
-- npx -y intermarche-drive-mcp --stdio
# direct invocation (smoke test, --http, MCP Inspector, etc.)
STORE_ID=09571 npx -y intermarche-drive-mcp --httpYou'll still need the cookie-sync extension (Step 2) — it lives outside npm
because Chromium requires a loaded-unpacked folder. Grab it from
the GitHub repo's extension/ folder
or from your package install at
$(npm root -g)/intermarche-drive-mcp/extension/.
State (cookies + picked slot) is persisted to
$HOME/.intermarche-drive-mcp/state.json so it survives npx cache resets.
Override with the STATE_FILE env var.
For maintainers: until
npm publishhas been run once, this path doesn't work yet — use Option B or C below. The package is wired (bin,filesallowlist,prepublishOnlyguard) and ready to ship.
Option B — Install from source
git clone https://github.com/TsRun/intermarche-drive-mcp.git
cd intermarche-drive-mcp
npm install
npm run buildSet the required env var and (optionally) override the defaults:
# required
export STORE_ID=09571 # your 5-digit store code
# optional — these are the defaults
export PORT=7801 # streamable-http MCP port
export EXTENSION_PORT=7800 # cookie-sync sidecar (loopback only)
export REQUEST_DELAY_MS=500 # rate limit per request — do not lower
export LOG_LEVEL=info
export CHECKOUT_URL=https://www.intermarche.com/courses-en-ligneRun the server:
# stdio transport (for Claude Desktop, Cursor, etc.)
node dist/server.js --stdio
# OR streamable-http transport (for MCP Inspector, smoke.sh, remote clients)
node dist/server.js --httpSkip to Step 2 — Install the cookie-sync extension.
Option C — Install with Docker
git clone https://github.com/TsRun/intermarche-drive-mcp.git
cd intermarche-drive-mcp
STORE_ID=09571 docker compose up -d --buildThe compose file exposes the MCP HTTP transport on localhost:7801 and the
cookie-sync sidecar on 127.0.0.1:7800 (loopback only — the extension talks to
this from your browser). Cookies and lists are persisted to ./data/.
Check it's healthy:
docker compose logs -f
# Expect: {"level":"info","msg":"server_ready","transport":"http","port":7801}Step 2 — Install the cookie-sync extension
The server doesn't accept passwords. Instead, a small unpacked Chromium
extension reads your *.intermarche.com cookies and posts them to the sidecar
on 127.0.0.1:7800 whenever you click Sync now.
- Open
chrome://extensions(oredge://extensions,brave://extensions). - Toggle Developer mode on.
- Click Load unpacked and pick the
extension/folder of this repo. - Log in to intermarche.com in any tab and set your store.
- Click the extension's toolbar icon → Sync now.
- The popup should report
✓ Logged in to <store name> (N cookies).
If you changed EXTENSION_PORT, see extension/README.md
for the two-line patch needed.
Step 3 — Register it with your MCP client
Claude Code (one command)
If you use the Claude Code CLI, register the server in one line — no JSON editing:
# stdio (recommended — local launch, one process per Claude Code session)
claude mcp add intermarche-drive \
--env STORE_ID=09571 \
-- node /absolute/path/to/intermarche-drive-mcp/dist/server.js --stdio
# OR HTTP, against a long-running server (e.g. docker compose up)
claude mcp add --transport http intermarche-drive http://localhost:7801/mcpThen verify in any Claude Code session:
/mcpYou should see intermarche-drive listed with its tools (whoami,
set_store, search_products, ...). To remove it later:
claude mcp remove intermarche-drive.
If you took Option A (npm), the stdio command is:
claude mcp add intermarche-drive --env STORE_ID=09571 -- npx -y intermarche-drive-mcp --stdio
Claude Desktop
Edit claude_desktop_config.json (Settings → Developer → Edit Config):
{
"mcpServers": {
"intermarche-drive": {
"command": "node",
"args": ["/absolute/path/to/intermarche-drive-mcp/dist/server.js", "--stdio"],
"env": {
"STORE_ID": "09571"
}
}
}
}Restart Claude Desktop. The Intermarché tools appear in the tool list (look for
whoami, set_store, search_products, etc.).
If you're running via Docker (HTTP transport), point Claude at the URL instead:
{
"mcpServers": {
"intermarche-drive": {
"url": "http://localhost:7801/mcp"
}
}
}OpenAI Codex CLI
Codex reads MCP server definitions from ~/.codex/config.toml. Append:
[mcp_servers.intermarche-drive]
command = "node"
args = ["/absolute/path/to/intermarche-drive-mcp/dist/server.js", "--stdio"]
env = { STORE_ID = "09571" }For an HTTP server (e.g. docker compose up), use:
[mcp_servers.intermarche-drive]
url = "http://localhost:7801/mcp"Start a Codex session (codex) and check the tool list with /mcp — same
verification flow as Claude Code. (Newer Codex builds also expose
codex mcp add ... as a one-liner; the TOML snippet above always works.)
Cursor
In ~/.cursor/mcp.json (or your workspace's .cursor/mcp.json):
{
"mcpServers": {
"intermarche-drive": {
"command": "node",
"args": ["/absolute/path/to/intermarche-drive-mcp/dist/server.js", "--stdio"],
"env": { "STORE_ID": "09571" }
}
}
}Open Cursor's command palette → Cursor Settings → MCP to reload.
Step 4 — Verify
In your MCP client, ask the agent "call whoami". You should get back
{ logged_in: true, cookie_count: <N>, store_id: "09571" }. If
logged_in: false, click Sync now in the browser extension again.
For an end-to-end check (live account, never spends money):
LIVE=1 STORE_ID=09571 SMOKE_STORE_ID=09571 ./smoke.shThe script ends by printing a checkout_url. Open it in your browser to click
Payer; this server has no place_order tool by design.
Publishing to npm (maintainer only)
Anyone with publish rights on intermarche-drive-mcp can ship a new version:
npm login # one time
npm version patch # or minor / major
npm publish # runs typecheck + lint + test + build first
git push --follow-tagsThe prepublishOnly script blocks publish on a red build, lint failure, or
test failure. files in package.json is an allowlist (dist/,
extension/, README.md, LICENSE) so source, tests, fixtures, and
.claude/ never ship.
Troubleshooting
logged_in: falseafter sync — verify the extension popup reports the same store as yourSTORE_ID. The sidecar rejects sync attempts where/api/pdv-inforeturns a different ref.add_to_cartreturns an empty cart — the active store doesn't sell that product in its Drive catalog. Callset_storeagainst a Drive-capable store (supportsDrive: truein the response) and search again.429errors — you have another tool or script hitting intermarche.com. This server's rate limit (1 req / 500ms) is enforced per process; concurrent processes share the upstream's budget. Stop the other client.- State file conflicts — delete
data/state.jsonto start fresh; the extension will repopulate it on the next sync.
Task
Build a Model Context Protocol server (spec: https://modelcontextprotocol.io) that exposes a set of tools for driving Intermarché's click-and-collect service. The server runs locally, holds a persistent authenticated session to intermarche.com, and performs actions on the user's behalf.
Clients (Claude Desktop, Cursor, any MCP-capable agent) connect over stdio or streamable-http
and call the tools the server exposes.
Tech stack
- TypeScript + Node 22 +
@modelcontextprotocol/sdk(official MCP SDK). - Playwright (chromium, headless by default) for browser automation.
- If a clean internal JSON API can be reverse-engineered, prefer
undicifetch calls and keep Playwright as fallback for login / captcha / pages without a clean API.
- If a clean internal JSON API can be reverse-engineered, prefer
- Zod for tool-input schemas (MCP SDK integrates with it natively).
- Persistent storage: single
data/state.json(session cookies, last store, shopping lists). - Dockerfile +
docker-compose.ymlfor headless deployment.
Transports
Support both:
- stdio — for Claude Desktop and most local MCP clients.
- streamable-http on
PORT(default7801) — for remote use and debugging with MCP Inspector.
The transport is chosen via CLI flag: intermarche-drive-mcp --stdio or --http.
Tools exposed
All tool names use snake_case. Every input is validated with Zod. Every response is JSON,
never a giant string blob.
This is intentionally minimal: search, fill cart, see/clear cart. Checkout (placing the order) is left to the human in the browser — the agent prepares the basket, you click "pay".
Session
| Tool | Inputs | Returns |
|---|---|---|
| whoami | — | { logged_in, cookie_count, store_id } |
| set_store | { storeId } (5-digit) | { ok, storeId, storeName, driveType, supportsDrive } |
Catalog
| Tool | Inputs | Returns |
|---|---|---|
| search_products | { query, limit?, page? } | { total, page, totalPages, products: [...] } |
Cart
| Tool | Inputs | Returns |
|---|---|---|
| get_cart | — | { storeCode, lines: [...], subtotal, fees, total } |
| add_to_cart | { product_id, qty } (1–99) | updated cart |
| update_cart_item | { product_id, qty } (0–99, 0 = remove) | updated cart |
| clear_cart | — | { ok: true } |
| list_slots | — | { storeId, slots: [{ slotId, deliveryType, day, start, end, available, ... }] } |
| pick_slot | { slotId } | { ok, slot: ConfirmedSlot } — persisted to state.json, no server reservation |
| prepare_checkout | — | { ready, summary, blockers, checkout_url } — open URL to click Pay |
Orders
| Tool | Inputs | Returns |
|---|---|---|
| list_orders | { limit?, months? } | array of { orderId, total, itemCount, pickupDate, pickupSlot, status, ... } |
Prepare-and-handoff checkout
prepare_checkout is deliberately not a place-order tool. There is no
place_order tool — the MCP server stops one click short of spending money.
The flow:
list_slots→pick_slot(validated and persisted tostate.json; no server-side reservation, intermarche.com has no such endpoint).prepare_checkout→ re-verifies the slot, computes totals, returns acheckout_url(configurable viaCHECKOUT_URL; defaults tohttps://www.intermarche.com/courses-en-ligne).- Open the URL in your real browser (the one with the active session), click Payer. 3DS / SCA happens in your bank's UI, not here.
Blockers (no_slot, slot_expired, etc.) are returned in the payload, not
thrown — the agent can pick a new slot without retrying.
Not in v1 (left to the user)
- Clicking Payer. By design, there is no automated order submit. The agent prepares the basket; you finish in your browser.
- Category browsing — deliberately out of scope. Add only when there's real demand.
get_order/reorder— order detail is server-rendered HTML on intermarche.com, not a JSON API. Would require either finding the SSR backend or scraping HTML.list_ordersalready exposes enough metadata (per-order total, item count, date, pickup slot, invoice URL) for an agent to summarize past orders.
Auth & credentials
This server does not handle your Intermarché password. Session cookies
are synced from your logged-in browser via a small Chromium extension at
extension/. Load it unpacked once; click the toolbar icon →
"Sync now" whenever cookies expire. See extension/README.md
for install steps.
Server-side, the cookies the extension POSTs are persisted to data/state.json
only after a successful round-trip against /api/pdv-info. On verification
failure, the previous cookies are restored — syncing a stale session won't
wipe a working one.
PlaywrightDriver is bundled for future flows that need browser automation
(e.g. handling pages without a clean JSON API), but the cookie-sync path does
not use it.
Rate limiting & politeness
- Hard cap of 1 request per 500ms against intermarche.com (configurable via env
REQUEST_DELAY_MS). - Retries: max 2, exponential backoff, only on 5xx and network errors. Never retry on 4xx.
- User-agent: a realistic Chrome UA, not a bot string. This is personal automation, not a crawler.
- No concurrent scraping across tools — serialize HTTP requests through a single queue.
Error handling
- Every tool returns either a success payload or throws an
McpErrorwith a clear message. - Network errors →
{ code: "upstream_unreachable", message, retryable: true }. - Auth errors →
{ code: "not_logged_in" }— the agent should callloginand retry. - Out-of-stock products during checkout → partial success with a list of skipped items.
Observability
- JSON logs to stderr (so they don't interfere with stdio transport).
- One log line per tool call:
{ tool, duration_ms, ok, args_summary }. - Optional
DEBUG=mcp:*flag for verbose Playwright traces saved todata/traces/.
Tests
- Vitest for unit tests on parsers, cart math, and schema validation.
- Playwright tests (tagged
@live) that hit a staging/test account — skipped in CI unlessLIVE=1is set. - Contract tests: spawn the MCP server and exercise every tool via the official MCP test client, asserting against recorded fixtures.
- Fixtures: record real intermarche.com HTML/JSON responses once, check them in under
tests/fixtures/, never hit the real site in unit tests.
Deliverables
src/— MCP server (TypeScript, strict mode, noany).src/tools/— one file per tool or per grouped-domain (catalog.ts,cart.ts, etc.).src/driver/— abstraction layer with two implementations:HttpDriver(internal API) andPlaywrightDriver(browser). Tools depend only on the abstractDriverinterface.Dockerfile,docker-compose.yml(with a volume fordata/).README.mdwith: install, configure, register with Claude Desktop (claude_desktop_config.jsonsnippet), register with Cursor, example prompts.CLAUDE.md— guidance for AI agents editing this repo.smoke.sh— end-to-end script: login → search → add to cart → dry-run checkout.- Example
claude_desktop_config.jsonsnippet.
What to skip
- No multi-account support. One user, one session.
- No scheduling/cron inside this server — let the user/agent drive timing.
- No price history / deal tracking (separate project).
- No UI. Pure MCP server.
- No web scraping of non-logged-in catalog at scale — this is for personal automation.
Legal / ToS
Automating your own grocery orders for personal use is the intended use. Do not:
- Scrape the catalog at scale for resale or redistribution.
- Run this server against someone else's account without permission.
- Hammer the site with concurrent requests.
The rate limiter exists for a reason.
Definition of done
You should be able to:
docker compose up -dand have a running MCP server over HTTP onlocalhost:7801.- Add the stdio entry to
claude_desktop_config.jsonand restart Claude Desktop. - In Claude, say "log me in to Intermarché, switch to store 09571, search for whole milk, add 2 of the cheapest in-stock option to my cart, pick a Saturday morning slot, and prepare checkout — don't actually pay yet" and have every step actually happen.
- Say "prepare checkout" and the agent calls
prepare_checkout. Open the returnedcheckout_urlin your browser and click Payer. smoke.shruns green and prints the samecheckout_url.
