{"id":47178546,"url":"https://github.com/taejin5314/ikea-mcp","last_synced_at":"2026-04-01T16:59:32.952Z","repository":{"id":344109472,"uuid":"1179224479","full_name":"taejin5314/ikea-mcp","owner":"taejin5314","description":"Read-only IKEA MCP server for product search, store stock lookup, and multi-store stock comparison.","archived":false,"fork":false,"pushed_at":"2026-03-15T04:25:34.000Z","size":112,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-03-16T19:44:01.436Z","etag":null,"topics":["claude-code","ikea","inventory","mcp","model-context-protocol","nodejs","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/taejin5314.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-11T20:18:11.000Z","updated_at":"2026-03-15T04:25:37.000Z","dependencies_parsed_at":"2026-03-16T09:01:17.150Z","dependency_job_id":null,"html_url":"https://github.com/taejin5314/ikea-mcp","commit_stats":null,"previous_names":["taejin5314/ikea-mcp"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/taejin5314/ikea-mcp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taejin5314%2Fikea-mcp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taejin5314%2Fikea-mcp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taejin5314%2Fikea-mcp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taejin5314%2Fikea-mcp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/taejin5314","download_url":"https://codeload.github.com/taejin5314/ikea-mcp/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taejin5314%2Fikea-mcp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30622079,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-17T08:10:05.930Z","status":"ssl_error","status_checked_at":"2026-03-17T08:10:04.972Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["claude-code","ikea","inventory","mcp","model-context-protocol","nodejs","typescript"],"created_at":"2026-03-13T07:01:03.002Z","updated_at":"2026-03-17T10:01:16.735Z","avatar_url":"https://github.com/taejin5314.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ikea-mcp\n\nRead-only MCP server for IKEA product search and in-store stock lookup.\n\n**Transports:** stdio (Claude Desktop / MCP CLI) · Streamable HTTP (remote clients)\n**License:** MIT · **No auth required to run locally**\n\n## Capabilities\n| Tool | What it does |\n|---|---|\n| `list_stores` | List known store IDs and labels, optionally filtered by country |\n| `search_products` | Search IKEA products by keyword |\n| `get_product_details` | Get details for a single product by item number |\n| `check_store_stock` | Check cash-and-carry stock at one store |\n| `check_multi_item_stock` | Check stock for multiple items at one store |\n| `compare_store_stock` | Compare stock across explicit stores or a country catalog |\n| `find_best_store_for_item` | Rank stores by in-stock quantity (optionally filter by country) |\n| `check_cart_availability` | Check whether all items in a shopping list are available at one store |\n| `find_best_store_for_cart` | Rank stores by cart fulfillment across multiple items |\n\n## MVP limitations\n- Uses unofficial public IKEA APIs — no SLA, may break without notice\n- Canada store coverage is complete (15 stores)\n- US coverage is incomplete — 4 small-format stores have unknown API IDs (Queens, Alpharetta, Indianapolis, Arlington)\n- San Francisco small-format store is intentionally excluded (known ID 3136 returns 405)\n- No extra stores are included\n- Cash-and-carry availability only — click-and-collect and home delivery not exposed\n- HTTP transport is open by default — set `API_KEY` env var to require `x-api-key` header on `/mcp`\n- Read-only — no cart, order, or account operations\n\n## Tools\n\n### `search_products`\n\nSearch IKEA products by keyword.\n\n**Input**\n| param | type | default | required |\n|---|---|---|---|\n| `query` | string | — | yes |\n| `countryCode` | string | `\"US\"` | no |\n| `langCode` | string | `\"en\"` | no |\n| `size` | number | `10` | no |\n\n**Output**\n```json\n{\n  \"total\": 97,\n  \"items\": [\n    {\n      \"itemNo\": \"20522046\",\n      \"name\": \"BILLY\",\n      \"typeName\": \"Bookcase\",\n      \"salesPrice\": { \"amount\": 69.99, \"currencyCode\": \"USD\" },\n      \"pipUrl\": \"https://www.ikea.com/us/en/p/...\",\n      \"ratingValue\": 4.8,\n      \"ratingCount\": 1234\n    }\n  ]\n}\n```\n\n---\n\n### `get_product_details`\n\nGet details for a single IKEA product by item number.\n\n**Input**\n| param | type | default | required |\n|---|---|---|---|\n| `itemNo` | string | — | yes |\n| `countryCode` | string | `\"US\"` | no |\n| `langCode` | string | `\"en\"` | no |\n\n**Output**\n```json\n{\n  \"itemNo\": \"20522046\",\n  \"name\": \"BILLY\",\n  \"typeName\": \"Bookcase\",\n  \"salesPrice\": { \"amount\": 79, \"currencyCode\": \"USD\" },\n  \"pipUrl\": \"https://www.ikea.com/us/en/p/billy-bookcase-white-20522046/\",\n  \"designText\": \"white\",\n  \"measureText\": \"31 1/2x11x79 1/2 \\\"\",\n  \"ratingValue\": 4.6,\n  \"ratingCount\": 2620\n}\n```\n\n\u003e `shortDescription` and `materials` are not available from the underlying API.\n\n---\n\n### `check_store_stock`\n\nCheck stock at a single IKEA store.\n\n**Input**\n| param | type | default | required |\n|---|---|---|---|\n| `itemNo` | string | — | yes |\n| `storeId` | string | — | yes |\n| `countryCode` | string | `\"US\"` | no |\n\n**Output**\n```json\n{\n  \"storeId\": \"399\",\n  \"availableForCashCarry\": true,\n  \"quantity\": 110,\n  \"messageType\": \"HIGH_IN_STOCK\",\n  \"errors\": null\n}\n```\n\nOn error (e.g. item not carried):\n```json\n{\n  \"storeId\": \"026\",\n  \"availableForCashCarry\": false,\n  \"quantity\": null,\n  \"messageType\": null,\n  \"errors\": [{ \"code\": 404, \"message\": \"Not found\", \"meaning\": \"item not stocked at this store\" }]\n}\n```\n\n---\n\n### `compare_store_stock`\n\nCompare stock for one item across multiple stores. Provide explicit `storeIds`, or use `countryCode` to expand to all catalog stores for that country. At least one of `storeIds` or `countryCode` is required.\n\n**Input**\n| param | type | default | required |\n|---|---|---|---|\n| `itemNo` | string | — | yes |\n| `storeIds` | string[] (min 2) | — | one of `storeIds`/`countryCode` |\n| `countryCode` | `\"US\"` \\| `\"CA\"` | — | one of `storeIds`/`countryCode` |\n| `sortBy` | `\"quantity\"` \\| `\"storeId\"` | — | no |\n\n`storeIds` takes precedence — if both are provided, `countryCode` only sets the IKEA API locale.\n`sortBy: \"quantity\"` sorts descending, null quantities last, `storeId` as tie-breaker. `sortBy: \"storeId\"` sorts ascending. Omitting `sortBy` preserves input order.\n\n**Examples**\n```json\n{ \"itemNo\": \"20522046\", \"storeIds\": [\"399\", \"026\", \"921\"] }\n```\n```json\n{ \"itemNo\": \"20522046\", \"countryCode\": \"CA\" }\n```\n\n**Output** — array of the same shape as `check_store_stock` (one entry per store).\n\n**Detecting partial failures:** rows with `errors` containing any code other than `404` indicate a store-level or API failure (e.g. `405` = invalid store ID). Rows with only `404` errors mean the item is simply not stocked at that store — this is expected, not a failure.\n\n---\n\n### `check_multi_item_stock`\n\nCheck cash-and-carry stock for multiple items at a single store in one call.\n\n**Input**\n| param | type | default | required |\n|---|---|---|---|\n| `storeId` | string | — | yes |\n| `itemNos` | string[] (min 1, max 20) | — | yes |\n\n**Output** — array of per-item stock entries in the same order as `itemNos`:\n\n```json\n[\n  {\n    \"itemNo\": \"20522046\",\n    \"storeId\": \"399\",\n    \"storeLabel\": \"399 (Burbank, CA)\",\n    \"availableForCashCarry\": true,\n    \"quantity\": 104,\n    \"messageType\": \"HIGH_IN_STOCK\",\n    \"errors\": []\n  }\n]\n```\n\nItems not stocked at that store appear with `availableForCashCarry: false`, `quantity: null`, and a 404 error entry. An invalid `storeId` (405) returns that error on every entry.\n\n---\n\n### `find_best_store_for_item`\n\nFind stores with the highest in-stock quantity for an item. Queries stores in parallel, excludes invalid stores (405), out-of-stock stores (404), and stores with unknown quantity. Results sorted by quantity descending; ties broken by `storeId` lexicographically.\n\n**Input**\n| param | type | default | required |\n|---|---|---|---|\n| `itemNo` | string | — | yes |\n| `storeIds` | string[] | all known stores | no |\n| `maxResults` | number | `3` (max 50) | no |\n| `countryCode` | `\"US\"` \\| `\"CA\"` | — | no |\n| `minQuantity` | number (int ≥ 1) | — | no |\n\n`storeIds` takes precedence. If only `countryCode` is given, searches all catalog stores for that country. If neither is given, searches all ~65 known stores. `minQuantity` excludes stores with quantity below the threshold.\n\n**Output** — array of matching stores, up to `maxResults`:\n```json\n[\n  {\n    \"storeId\": \"399\",\n    \"storeLabel\": \"399 (Burbank, CA)\",\n    \"availableForCashCarry\": true,\n    \"quantity\": 104,\n    \"messageType\": \"HIGH_IN_STOCK\"\n  }\n]\n```\n\nReturns `[]` if no store has the item in stock. \"All known stores\" means the ~65 US and Canada entries in `src/data/stores.ts`.\n\n**Note on failures:** stores that return a store-level error (405 invalid store ID) are silently excluded from results rather than appearing as rows. Use `compare_store_stock` with the same `storeIds` to inspect per-store `errors` directly.\n\n---\n\n### `check_cart_availability`\n\nCheck whether all items in a shopping list are available in sufficient quantity at a single IKEA store.\n\n**Input**\n| param | type | default | required |\n|---|---|---|---|\n| `storeId` | string | — | yes |\n| `items` | array of `{ itemNo, quantity }` | — | yes |\n| `items[].itemNo` | string | — | yes |\n| `items[].quantity` | number | `1` | no |\n\n**Output**\n```json\n{\n  \"storeId\": \"399\",\n  \"storeLabel\": \"399 (Burbank, CA)\",\n  \"allSufficient\": true,\n  \"items\": [\n    {\n      \"itemNo\": \"20522046\",\n      \"quantity\": 2,\n      \"inStock\": 42,\n      \"sufficient\": true,\n      \"eligibleForStockNotification\": false,\n      \"errors\": []\n    }\n  ]\n}\n```\n\n`allSufficient` is `true` only when every item has `sufficient: true`. Items not stocked appear with `inStock: null` and a 404 error. An invalid `storeId` (405) propagates to all items.\n\n---\n\n### `find_best_store_for_cart`\n\nFind the best store to buy multiple items in one trip. Ranks stores by how many cart items are available in sufficient quantity, then by total in-stock sum. Optionally filter by `countryCode` or provide explicit `storeIds`.\n\n**Input**\n| param | type | default | required |\n|---|---|---|---|\n| `items` | array of `{ itemNo, quantity }` | — | yes |\n| `items[].itemNo` | string | — | yes |\n| `items[].quantity` | number | `1` | no |\n| `storeIds` | string[] | — | no |\n| `countryCode` | `\"US\"` \\| `\"CA\"` | — | no |\n| `maxResults` | number | `3` (max 50) | no |\n\n`storeIds` takes precedence. If only `countryCode` is given, searches all catalog stores for that country. If neither is given, searches all ~65 known stores.\n\n**Output** — array of stores ranked by cart fulfillment, up to `maxResults`:\n```json\n[\n  {\n    \"storeId\": \"399\",\n    \"storeLabel\": \"399 (Burbank, CA)\",\n    \"allSufficient\": true,\n    \"fulfilledCount\": 3,\n    \"totalCount\": 3,\n    \"items\": [\n      { \"itemNo\": \"20522046\", \"quantity\": 2, \"inStock\": 42, \"sufficient\": true },\n      { \"itemNo\": \"40477340\", \"quantity\": 1, \"inStock\": 5, \"sufficient\": true },\n      { \"itemNo\": \"89268919\", \"quantity\": 1, \"inStock\": 12, \"sufficient\": true }\n    ]\n  }\n]\n```\n\n`fulfilledCount` = number of items with `sufficient: true`. Sorting: `fulfilledCount` desc → total stock desc → `storeId` asc. Stores with invalid IDs (405) are excluded.\n\n---\n\n## Example workflows\n\n### 1. Search → inspect → check one store\n\n```\n1. search_products       { \"query\": \"BILLY bookcase\" }\n   → pick itemNo from results, e.g. \"20522046\"\n\n2. get_product_details   { \"itemNo\": \"20522046\" }\n   → confirms name, price, dimensions before checking stock\n\n3. check_store_stock     { \"itemNo\": \"20522046\", \"storeId\": \"399\" }\n   → { \"availableForCashCarry\": true, \"quantity\": 95, \"messageType\": \"HIGH_IN_STOCK\" }\n```\n\n### 2. Shopping list at one store\n\nCheck whether several items are available in a single trip:\n\n```json\n{\n  \"tool\": \"check_multi_item_stock\",\n  \"storeId\": \"399\",\n  \"itemNos\": [\"20522046\", \"40477340\", \"89268919\"]\n}\n```\n\nReturns one entry per item in the same order — items not stocked appear with `availableForCashCarry: false` and a 404 error.\n\n### 3. Best store from a mixed US + Canada subset\n\n```json\n{\n  \"tool\": \"find_best_store_for_item\",\n  \"itemNo\": \"20522046\",\n  \"storeIds\": [\"399\", \"039\", \"216\", \"149\", \"026\"],\n  \"maxResults\": 3\n}\n```\n\nReturns the top 3 stores by in-stock quantity across the mixed US/Canada subset. Omit `storeIds` to search all ~65 known stores.\n\n### 4. Best store for a shopping list\n\nFind which store can fulfill the most items from a multi-item cart:\n\n```json\n{\n  \"tool\": \"find_best_store_for_cart\",\n  \"items\": [\n    { \"itemNo\": \"20522046\", \"quantity\": 2 },\n    { \"itemNo\": \"40477340\", \"quantity\": 1 },\n    { \"itemNo\": \"89268919\", \"quantity\": 1 }\n  ],\n  \"countryCode\": \"CA\",\n  \"maxResults\": 3\n}\n```\n\nReturns the top 3 Canada stores ranked by how many items they can fully supply. Use `check_cart_availability` to then verify exact quantities at the chosen store.\n\n---\n\n## Build and test\n\n```bash\nnpm install\nnpm run build        # tsc → dist/\nnpm run typecheck    # type-check without emit\nnpm test             # unit tests\nnode smoke.mjs       # end-to-end stdio smoke test\n```\n\n`smoke.mjs` exercises all 4 tools against the live IKEA API and prints pass/fail lines to stdout.\n\n## Transports\n\n**stdio** (default — for Claude Desktop / MCP CLI):\n```bash\nnpx ikea-mcp          # after npm install (uses bin entry)\nnode dist/index.js    # after local build\nnpm run dev           # dev (tsx, no build needed)\n```\n\n**Streamable HTTP** (for remote / network clients):\n```bash\nnode dist/http.js          # listens on http://localhost:3000/mcp\nPORT=8080 node dist/http.js\n# or during dev:\nnpm run dev:http\n```\n\nRequests must include `Accept: application/json, text/event-stream`. Stateless — no session management.\n\n## Deploy (HTTP transport)\n\nTested target: **Railway** (also works on Render, Heroku, or any Procfile-aware host).\n\n```bash\n# 1. build\nnpm install \u0026\u0026 npm run build\n\n# 2. run (Procfile: web: node dist/http.js)\n#    PORT is set automatically by the host\nnode dist/http.js\n```\n\nThe `Procfile` in the repo root declares `web: node dist/http.js`. `PORT` is read from the environment (default `3000`). No other env vars required.\n\nEndpoints after deploy:\n- `POST /mcp` — MCP Streamable HTTP (requires `Accept: application/json, text/event-stream`)\n- `GET /health` — returns `{\"status\":\"ok\"}`\n\n\u003e **Security note:** Set `API_KEY` to protect the `/mcp` endpoint. Requests without a matching `x-api-key` header return 401. `/health` is always open. The server is read-only — no cart, order, or account operations are possible.\n\u003e\n\u003e ```bash\n\u003e API_KEY=your-secret node dist/http.js\n\u003e ```\n\n## Connecting a local MCP client (stdio)\n\n**Claude Desktop** (`claude_desktop_config.json`):\n```json\n{\n  \"mcpServers\": {\n    \"ikea-mcp\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"ikea-mcp\"]\n    }\n  }\n}\n```\n\n---\n\n## Connecting a remote MCP client (HTTP)\n\nPoint your MCP client at `https://\u003cyour-host\u003e/mcp`.\n\n**Claude Desktop** (`claude_desktop_config.json`):\n```json\n{\n  \"mcpServers\": {\n    \"ikea-mcp\": {\n      \"type\": \"http\",\n      \"url\": \"https://\u003cyour-host\u003e/mcp\"\n    }\n  }\n}\n```\n\n**`.mcp.json`** (project-local, Claude Code):\n```json\n{\n  \"mcpServers\": {\n    \"ikea-mcp\": {\n      \"type\": \"http\",\n      \"url\": \"https://\u003cyour-host\u003e/mcp\"\n    }\n  }\n}\n```\n\n**Manual / curl** (for debugging):\n```bash\ncurl -X POST https://\u003cyour-host\u003e/mcp \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}'\n```\n\nThe `Accept: application/json, text/event-stream` header is required by the MCP SDK — requests without it will be rejected with a `-32000` error.\n\n## Store IDs\n\nStore metadata (ID → city label) lives in `src/data/stores.ts`. ~50 US stores confirmed from `ikea.com/us/en/stores/` pages; 15 Canada stores confirmed from `ikea.com/ca/en/stores/` pages (all probed against the stock API).\n\nConfirmed compatible `storeId` formats:\n- Standard 3-digit: `\"399\"` (Burbank, CA, US), `\"216\"` (Calgary, AB, CA)\n- Leading-zero 3-digit: `\"026\"` (Canton, MI, US), `\"039\"` (Montreal, QC, CA)\n- 4-digit: `\"921\"` (Brooklyn, NY, US), `\"1129\"` (Syracuse, NY, US)\n\nAn invalid or unsupported `storeId` returns a 405 error in the `errors` array.\n\n## Limitations\n\n- Uses unofficial public IKEA APIs — no SLA, no auth required, may break without notice.\n- Read-only: no cart, no order, no account operations.\n- Country-wide fan-out (`countryCode: \"US\"` ≈ 52 stores, `\"CA\"` ≈ 15) is capped at 10 concurrent requests and retries once on transient 5xx/network errors.\n- Click-and-collect and home-delivery availability are not exposed (cash-and-carry only).\n- `size` in `search_products` is capped by IKEA's API (observed max ~24 per page; `total` reflects the full catalogue count).\n- US and Canada only — no other countries supported.\n\n## Item numbers\n\n`itemNo` fields accept several formats — all are normalised to 8 digits internally:\n\n| Input | Normalised |\n|---|---|\n| `\"20522046\"` | `\"20522046\"` |\n| `\"522132\"` | `\"00522132\"` |\n| `\"005.221.32\"` | `\"00522132\"` |\n| `\"5-221-32\"` | `\"00522132\"` |\n\n6- and 7-digit inputs are left-padded to 8 digits. 8- and 9-digit inputs are kept as-is. Values outside 6–9 digits after stripping are rejected.\n\n## Supported countries\n\n| Country | Code | Store count |\n|---|---|---|\n| United States | `US` | ~52 |\n| Canada | `CA` | ~15 |\n\nUse `list_stores` to get the current catalog. Some store IDs in the catalog are unverified — they are listed but may return 405 from the stock API.\n\n## Rate limits \u0026 reliability\n\n- Fan-out requests (country-wide `compare_store_stock` / `find_best_store_for_item`) are capped at **10 concurrent** outbound requests.\n- `fetchJson` retries **once** after 500 ms on 5xx, 429, or network errors. 404 and 405 are not retried (they are semantic responses, not transient failures).\n- `Retry-After` header is respected for 429 responses.\n- Do not use in high-frequency loops — the upstream IKEA API has no published rate limit but will block repeated bursts.\n\n## Troubleshooting\n\n| Symptom | Likely cause |\n|---|---|\n| `405` in `errors` | Invalid `storeId` — use `list_stores` to find valid IDs |\n| `404` in `errors` | Item not stocked at that store |\n| Empty `find_best_store_for_item` result | No store has the item in stock, or `minQuantity` is too high |\n| Slow `countryCode` query | Normal — fan-out to all country stores (capped at 10 concurrent) |\n| `itemNo` validation error | Input must resolve to 6–9 digits; see Item numbers above |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftaejin5314%2Fikea-mcp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftaejin5314%2Fikea-mcp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftaejin5314%2Fikea-mcp/lists"}