Skip to main content
Glama

Ge Claude dina recept. Få rätt varor, i rätt mängd, i din kundvagn — och veta var det är billigast.

En MCP-server för Willys, Hemköp, Coop, City Gross och Mathem som gör svensk matplanering till något du delegerar i stället för något du gör.

TypeScript Node MCP License

Vad det här är

Ingen av de svenska matkedjorna har ett publikt API. Den här servern kartlägger deras faktiska endpoints och exponerar dem som MCP-verktyg — sökning, jämförprisrankning, prisjämförelse mellan kedjor, butiksregister, och för Willys även inloggning, kundvagn och orderhistorik.

Men det intressanta ligger ovanpå: kundvagnen behandlas som en projektion av vad dina recept kräver. Inte som en hög varor någon lagt i en gång. Och samma plan kan prissättas i alla fem kedjor.

Kedja

Sök & pris

Butik

Kundvagn & ordrar

Willys

✅

valfri, samma pris i hela landet

✅

Hemköp

✅

valfri, priser per butik

—

Coop

✅

krävs, välj via postnummer

—

City Gross

✅

valfri, smalnar av sortimentet

—

Mathem

✅

finns inte, en nationell prislista

—

ICA stöds inte: deras API ligger bakom ett botskydd som tillåter ungefär fem anrop per fem minuter, för lite för att planera en korg. Detaljer per kedja: docs/kedjor.md.

Related MCP server: Willys MCP Server

Se den göra något

Du: Här är tre recept för veckan — pannkakor, omelett och äggröra.

Claude: (lägger till recepten, räknar om korgen) Du behöver 13 ägg totalt. Billigast blir ett 15-pack för 36,81 kr — 2 ägg över. Jag har också hittat vispgrädde: 4× 2,5 dl för 43,52 kr täcker dina 8 dl.

Summa 80,33 kr. Vill du att jag lägger det i vagnen?

Du: Ja. Och lägg till en gratäng som behöver 5 dl grädde.

Claude: (behovet ändras, korgen räknas om) Grädden går från 8 till 13 dl. Samma produkt är fortfarande billigast, så jag höjer antalet från 4 till 6 förpackningar — 65,28 kr. Äggen är oförändrade och rörs inte.

Servern har inget kassaverktyg. Den kan fylla kundvagnen men aldrig lägga en order. Det sista klicket är alltid ditt.

Du: Vad kostar pastan i de andra kedjorna?

Claude: (samma plan prissatt i fem kedjor, inget ändras) Willys 61,30 kr · Coop Bäckebol 64,40 kr · Hemköp 69,35 kr · Mathem 70,10 kr · City Gross 89,85 kr. Willys är billigast med 3,10 kr till Coop.

Riktiga priser från 2026-10-14: 500 g köttfärs, 400 g krossade tomater och 500 g spaghetti.

Det svåra problemet: du kan inte köpa 13 ägg

Bara förpackningar av dem. Det låter trivialt och är det inte.

En vanlig prissökning rankar på jämförpris — kr per kilo, kr per liter, kr per styck. Men när mängden är känd är det fel fråga. Det som avgör är vad just ditt behov kostar när du bara kan köpa hela förpackningar.

Strategi

Antal

Får

Över

Kostnad

6-pack

3×

18 st

5

74,70 kr

10-pack

2×

20 st

7

69,80 kr

24-pack

1×

24 st

11

62,00 kr ✅

Konstruerat exempel med satta priser, hämtat ur testsviten. I Willys faktiska sortiment löses 13 ägg av ett 15-pack med bara 2 över — men principen är densamma.

24-packet vinner på riktiga pengar trots sämst antal spillda ägg. Servern räknar igenom varje täckningsstrategi och rangordnar på total kostnad.

Men den väljer inte åt dig när avvägningen är verklig. Överstiger billigaste vägen behovet med mer än 50 %, och finns ett märkbart tätare alternativ, returneras en fråga i stället för ett beslut:

Ägg 24-pack är 7,80 kr billigare men ger 11 st över behovet. Ägg 10-pack ger bara 7 st över. Vill du ha det billigare med överskott?

Det är hushållets avvägning, inte serverns. Rader med obesvarad fråga hamnar aldrig i vagnen av misstag.

Hur behoven hålls ihop

flowchart LR
    R1["Recept: Pannkakor<br/>6 ägg"] --> L
    R2["Recept: Omelett<br/>4 ägg"] --> L
    R3["Recept: Äggröra<br/>3 ägg"] --> L
    L["Behovsregister<br/><b>13 ägg</b>"] --> C
    C["Täckningsmatte<br/>1× 15-pack"] --> D
    D{"Diff mot<br/>kundvagnen"} --> A["Byt 6-pack<br/>→ 15-pack"]

Varje recepts bidrag lagras separat. Det ger tre egenskaper som en löpande summa inte kan ge:

  • Härkomst gratis. 13 ägg är inte ett tal utan Pannkakor:6 + Omelett:4 + Äggröra:3. Claude kan förklara en ändring, inte bara meddela den.

  • Borttagning är en radering, inte ett försök att subtrahera mängder som redan rundats upp till förpackningar.

  • Omräkning, inte lappning. Hela korgen härleds ur registret varje gång. Det är därför 6-packet byts ut när behovet växer, i stället för att ett till staplas på.

plan_apply skickar bara differensen. Rätt vara i rätt antal ger inget anrop alls, så det är gratis att köra om efter varje nytt recept.

Verktyg

Nästan alla verktyg tar chain. Claude frågar vilken kedja du handlar i om du inte sagt det — servern väljer aldrig åt dig.

Installation

npm install
npm run build

Koppla in i Claude Code:

claude mcp add willys --env WILLYS_HOME=$HOME/.willys-mcp -- node /absolut/sökväg/willys-mcp/dist/index.js

…eller i Claude Desktop:

{
  "mcpServers": {
    "willys": {
      "command": "node",
      "args": ["/absolut/sökväg/till/willys-mcp/dist/index.js"],
      "env": { "WILLYS_HOME": "/Users/dittnamn/.willys-mcp" }
    }
  }
}

Sök, priser och jämförelser fungerar direkt, utan konto. För kundvagn och orderhistorik hos Willys skriver du "hjälp mig komma igång med Willys" i en chatt. Claude frågar vilken ort du handlar i, slår upp din butik, skapar konfigurationsfilen och öppnar den åt dig. Det enda du gör själv är att fylla i två rader.

WILLYS_HOME styr var .env och sessionsdatabasen hamnar. Utan den ligger de i projektmappen, vilket är bekvämt i en checkout men gör att de följer med om mappen någon gång byts ut.

Fullständig guide: INSTALL.md

IMPORTANT

För kundvagnen måste ditt Willys-konto ha ettlösenord. BankID går inte att automatisera — servern loggar in genom att fylla i ett formulär. Har du bara använt BankID måste du skapa ett lösenord först, och Claude förklarar hur.

Arkitektur

src/
├── index.ts          MCP-entrypoint: transport + wiring (tunn)
├── setup.ts          förstagångsdiagnostik
├── tools/            ett verktyg = ett objekt, grupperat per domän
│   ├── registry.ts   samlar grupperna, äger dispatch (validering, auth, felgräns)
│   ├── kit.ts        Tool-kontrakt, Ctx, defineTool, svarshjälpare
│   ├── session.ts · search-tools.ts · cart-tools.ts · orders-tools.ts
│   ├── stores-tools.ts · setup-tools.ts · planner-tools.ts
│   ├── schema-parts.ts  delade Zod-fragment
│   └── json-schema.ts   Zod → JSON Schema för manifestet
├── planner/
│   ├── ledger.ts     behovsregister med härkomst per recept
│   └── reconcile.ts  bygger korgen, diffar mot vagnen
├── chains/           ChainAdapter-kontraktet, registret, delad rangordning och butiksregister
├── domain/           ren matte: volym, täckning, produktmodell, namnmatchning, jämförpris
├── upstream/         HTTP-klienter, en mapp per plattform, en fil per resurs
│   ├── axfood/       Willys + Hemköp (samma API, två värdar)
│   ├── coop/         API-nycklarna läses ur coop.se vid första anropet
│   ├── citygross/
│   └── mathem/
├── auth/             Puppeteer-inloggning, sessionslagring
├── net/              generisk URL-hämtare (fetch_url)
├── core/             http, result, logger, env
└── constants/        endpoints, gränsvärden, instructions

En adapter per plattform, inte per butik: Willys och Hemköp är samma Axfood-API, och skillnaden mellan dem är en bas-URL. Rangordning och filter ligger i chains/ och gäller alla kedjor lika.

domain/ och planner/ledger.ts rör aldrig nätverket. Täckningsmatten och behovsregistret är rena funktioner mot SQLite — vilket är varför de har riktiga tester som kör på under en sekund utan att logga in någonstans.

Designprinciper

Fem beslut som formar resten av kodbasen.

Servern gissar aldrig tyst. Filtreras varor bort står det hur många och varför — djurmat, fel namn, ej köpbara. Matchar en kategorifacett ingenting görs om­försök utan den och svaret säger vilka facetter som faktiskt finns. Ett tyst nollresultat är värre än ett fel — det ser ut som att hyllan är tom.

Beslut med verklig avvägning returneras som frågor. Överskott mot pris är inte serverns val. Samma sak när två recept vill ha 2 st tomat och 400 g tomat: det blir en konflikt att lösa, inte en påhittad omräkning.

Farliga tillstånd görs omöjliga i protokollet. Det finns inget kassaverktyg, så servern kan inte beställa. plan_apply har dryRun: true som default. setup_init tar inte emot lösenord som argument — inte som en regel modellen ska följa, utan för att schemat vägrar. Det är starkare än att skriva "gör inte så".

Zod validerar på riktigt. MCP-SDK:n validerar inte argument mot schemat en server annonserar — det är dokumentation, inte kontroll. Varje anrop parsas före dispatch. Utan det går en påhittad quantity rakt in i kundvagnen, och för add_to_cart betyder det riktiga varor.

Fel bär en klass hela vägen ut. AUTH_EXPIRED, SHAPE, NOT_FOUND, SETUP_REQUIRED, UPSTREAM. Modellen kan skilja "sessionen dog" från "kedjan ligger nere" från "en människa måste redigera en fil", och agera olika på varje.

Servern rangordnar själv. Bara Axfood sorterar rätt på jämförpris. City Gross ignorerar kampanjpris, Mathem kan inte sortera alls, och Coop har jämförpriser som är fel med en faktor tusen. Servern hämtar hela träffmängden, kontrollerar jämförpriset mot pris ÷ förpackning och sorterar.

Fällor som redan kostat tid

Dokumenterade i koden, med datum, så ingen återinför dem.

  • sort=price:asc rankar en portionsrätt på 16 kr över ett kilo köttfärs på 69 kr. Sortera på compareprice:asc.

  • Facettfilter ligger i q, inte i en egen parameter: q=köttfärs:category:Kött. Utan kategorin är billigaste träffen på "köttfärs" en Findus köttfärssås.

  • Fritextsök utan mustContain ger självsäkert nonsens. "torkad dragon" matchade ett torkat grisöra sålt som hundtugg — det var billigast med ordet "torkad".

  • Näringsvärden ligger i nutritionsFactList, inte nutritionFacts (tom sträng på varje produkt som inspekterats).

  • Willys svarar 400, inte 404, för en produktkod som inte finns.

  • Kundvagnen har ingen delete-endpoint. Borttagning är en add med quantity: 0.

  • Varje endpoint som innehåller ett Next.js build-id är en tidsinställd bomb. Tre är redan döda och ligger kvar i DEAD_ENDPOINTS som varning.

  • "köttfärs" hos Coop rankade kattmat först. Den var billigast av allt med kött.

  • City Gross totalCount räknar recept som träffar: "köttfärs" ger 101 träffar och 9 produkter.

  • Coop-sök mot en butik utan e-handel ger noll träffar, inte ett fel.

  • City Gross produktkoder är Axfoods: samma kod är samma vara hos Willys, Hemköp och City Gross.

Vad den inte gör

  • Beställer inte. Inget kassaverktyg finns.

  • Fungerar inte i claude.ai i webbläsaren. En webbsida kan inte starta program på din dator. Claude Desktop eller Claude Code.

  • Stödjer inte BankID.

  • Stödjer inte ICA.

  • Har ingen kundvagn för Hemköp, Coop, City Gross eller Mathem än.

  • Är inte officiell. Endpoints är kartlagda mot produktion och kan sluta fungera utan förvarning.

Utveckling

npm run typecheck      # enda verkliga porten före commit
npm run test:coverage  # täckningsmatte, inget nätverk
npm run test:planner   # behovsregister, inget nätverk
npm run test:volume    # 44 verkliga förpackningsformat
npm run test:chains    # mappning per kedja, rangordning, djurmat, jämförpriskontroll
npm run test:coop      # Coops nyckelhämtning och rotation, med stubbad fetch
npm run test:releases  # release notes och tombstones för gamla verktygsnamn
npm run smoke          # end-to-end över stdio, kräver session

Konventioner och fallgropar för framtida ändringar: CLAUDE.md Endpoint-kartläggning: docs/kedjor.md · Willys i detalj: docs/endpoints.md Ändringar per version: docs/CHANGELOG.md — AI-klienter läser samma fil via release_notes.

Förbehåll

Ett privat verktyg som automatiserar ett konto du själv äger och läser publika priser. Respektera kedjornas användarvillkor, kör det inte mot konton du inte förfogar över, och undvik anropsvolymer som liknar skrapning.


MIT · LICENSE

Available Tools

31 tools
willys_add_to_cartC

Lägg en produkt i kundvagnen.

Inputs: sessionId (required), productCode (required), quantity.

willys_check_authB

Kontrollera om en session fortfarande är giltig.

Inputs: sessionId (required).

willys_fetch_urlA

Hämta en webbsida från ANVÄNDARENS dator och få tillbaka strukturerad data (JSON-LD, t.ex. schema.org Recipe) plus ren text. Kör detta FÖRST när du behöver läsa en URL — det går via användarens hem-IP och tar sig förbi block som din egen hämtning fastnar på, och det är billigare. Faller din egen webbläsning tillbaka bara om detta misslyckas. Renderar JS-sidor automatiskt vid behov.

Inputs: url (required), render.

willys_find_cheapestA

Sök och rangordna på jämförpris (kr/kg, kr/l, kr/st), sorterat av Willys över hela träffmängden. Returnerar tillgängliga kategorier så du kan smalna av — utan kategori rankas köttfärssås som köttfärs.

Inputs: sessionId (required), need, limit, query, +7 more.

willys_find_storesB

Slå upp butiker på ort, namn eller postnummer. Läser ur lokal cache (hela kedjan, 254 butiker).

Inputs: query (required), limit, onlyClickAndCollect.

willys_get_campaignsB

Butiksspecifika kampanjer och erbjudanden. Ersätter den döda get_offers.

Inputs: sessionId (required), page, size, storeId.

willys_get_cartC

Hämta kundvagnens innehåll.

Inputs: sessionId (required).

willys_get_customer_infoC

Hämta kundprofil.

Inputs: sessionId (required).

willys_get_frequent_productsB

Vanligast köpta varor, härledda ur din faktiska orderhistorik (inte CMS-sidan, som saknar produktdata).

Inputs: sessionId (required), limit, scanOrders.

willys_get_order_detailsC

Alla varurader i en tidigare order.

Inputs: orderId (required), sessionId (required).

willys_get_ordersC

Hämta orderhistorik, sidad.

Inputs: sessionId (required), limit, offset.

willys_get_product_detailC

Detaljerad produktinformation för en artikelkod.

Inputs: sessionId (required), productCode (required).

willys_get_storeB

Detaljer för en butik: öppettider, plockavgift, fri frakt-gräns och länk till butikens erbjudandeblad.

Inputs: storeId (required).

willys_loginA

Logga in på Willys och få ett sessionId tillbaka. Säkert att anropa: servern läser uppgifterna ur .env, de når aldrig din kontext (se read_doc med help/security.md). Återanvänder en giltig session (24h); force=true tvingar ny inloggning.

Inputs: force.

willys_logoutC

Avsluta sessionen.

Inputs: sessionId (required).

willys_nearest_storesB

Närmaste butiker till en koordinat, sorterade på avstånd.

Inputs: lat (required), lng (required), limit, onlyClickAndCollect.

willys_open_fileA

Öppna en lokal fil i användarens standardeditor. Använd för att öppna .env åt dem vid uppsättning — sökvägen står i svaret från willys_setup. En chattklient gör inte filsökvägar klickbara. Läser inte innehållet.

Inputs: path (required).

willys_plan_add_recipeA

Lägg ett recepts ingredienser i behovsregistret. Mängder slås ihop per vara över alla recept i planen, så tre recept som vill ha ägg blir ETT behov. Rör inte kundvagnen — kör willys_plan_apply för det.

Inputs: recipe (required), sessionId (required), ingredients (required), plan.

willys_plan_applyA

Synka kundvagnen mot planen. Skickar BARA differensen: rätt vara i rätt antal ger inget anrop, växande behov byter förpackning eller höjer antalet. dryRun=true (default) visar ändringarna utan att röra vagnen.

Inputs: sessionId (required), plan, dryRun, acceptMatch, +2 more.

willys_plan_clearA

Nollställ planen. Tömmer INTE kundvagnen — den behåller det som redan lagts i.

Inputs: confirm (required), plan.

willys_plan_remove_ingredientA

Ta bort EN ingrediens ur ett recept (t.ex. en skafferivara användaren redan har hemma), utan att röra receptets övriga rader.

Inputs: query (required), recipe (required), plan.

willys_plan_remove_recipeA

Ta bort ett recept ur planen. Behoven räknas om; varor som bara det receptet krävde försvinner ur den föreslagna korgen.

Inputs: recipe (required), plan.

willys_plan_set_ingredientA

Lägg till eller rätta EN ingrediens i ett recept utan att röra de andra. Använd detta för att fixa en felmatchad rad (t.ex. skärpa mustContain) i stället för att skicka om hela receptet med willys_plan_add_recipe.

Inputs: recipe (required), sessionId (required), ingredient (required), plan.

willys_plan_statusA

Visa vad planen kräver, vilken produkt och mängd som föreslås för varje vara, vad det kostar, och vilka val som väntar på svar om överskott. Läser bara — ändrar varken plan eller kundvagn. Kör detta innan willys_plan_apply.

Inputs: sessionId (required), plan, preferTight, acceptSurplus.

willys_read_docA

Läs ett dokument som följer med servern (t.ex. "help/setup.md"). Deterministiskt begränsad till serverns docs/-mapp — kan inte läsa .env, sessioner eller något annat på datorn, så den är säker att anropa fritt. De korta instruktionerna pekar hit för detaljer så de inte belastar varje session. Tillgängliga: help/setup.md, help/security.md, help/search.md, help/planning.md, help/errors.md.

Inputs: path (required).

willys_refresh_storesC

Hämta om butiksregistret från Willys.

Inputs: no named parameters.

willys_remove_from_cartB

Ta bort en produkt ur kundvagnen.

Inputs: sessionId (required), productCode (required).

willys_search_suggestionsB

Sökförslag (autocomplete) för en term.

Inputs: term (required), sessionId (required).

willys_setupA

Kontrollera om servern är konfigurerad på den här datorn och få exakta steg om något saknas, med full sökväg till filen. Kräver ingen session. Kör detta först om willys_login svarar SETUP_REQUIRED. Se read_doc med help/setup.md.

Inputs: no named parameters.

willys_setup_initA

Skapa .env åt användaren och skriv in butiks-id, med rättigheter 600. Returnerar sökvägen till filen. Öppna den sedan med willys_open_file så användaren kan fylla i sina uppgifter.

Inputs: storeId.

Tool Schema Changelog

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

Loading tool schema changelog…

    TDQS

    B3.2/5.0

    Scored across 31 tools

    Disambiguation4/5

    The tools span distinct domains (stores, products, cart, orders, planning) with clear purposes. Some pairs like find_stores vs nearest_stores and search vs find_cheapest could be confused, but descriptions clarify the specific use cases. Overall, a capable agent can differentiate most tools without difficulty.

    Naming Consistency4/5

    Most tools follow a willys_verb_noun pattern (e.g., willys_get_cart, willys_plan_apply) with snake_case throughout. Minor deviations like willys_search, willys_login, and willys_setup_init don't break the pattern significantly. The naming is predictable and readable.

    Tool Count3/5

    With 31 tools, the server feels heavy, especially when including meta-tools like willys_fetch_url and willys_open_file that aren't grocery-specific. The broad scope (stores, products, cart, orders, planning, auth, setup) justifies many tools, but 31 is above the typical well-scoped range. It's borderline acceptable.

    Completeness4/5

    The tool surface covers the main workflows: store lookup, product search, cart management, order history, customer info, and meal planning. Missing explicit cart quantity updates or recipe listing can be worked around via add/remove and plan status. Overall, no critical dead ends for the core domain.

    Maintenance

    ActivityMaintained
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      B
      maintenance
      An MCP server for programmatically interacting with the Oda grocery shopping platform. It enables users to search for products, browse results, and manage their shopping cart contents through natural language.
      22
      -
    • A
      license
      A
      quality
      B
      maintenance
      An MCP server for Nordic grocery shopping that finds deals across supermarkets in Denmark, Norway, Sweden, and Finland, plans weekly dinners around cheap ingredients, and generates shopping lists grouped by store.
      18
      48
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      An MCP server for grocery shopping at Kroger-owned stores, enabling product search, store finder, cart management, and more through AI assistants.
      4 npm
      MIT