Content
# ikea-mcp
Read-only MCP server for IKEA product search and in-store stock lookup.
**Transports:** stdio (Claude Desktop / MCP CLI) · Streamable HTTP (remote clients)
**License:** MIT · **No auth required to run locally**
## Capabilities
| Tool | What it does |
|---|---|
| `list_stores` | List known store IDs and labels, optionally filtered by country |
| `search_products` | Search IKEA products by keyword |
| `get_product_details` | Get details for a single product by item number |
| `check_store_stock` | Check cash-and-carry stock at one store |
| `check_multi_item_stock` | Check stock for multiple items at one store |
| `compare_store_stock` | Compare stock across explicit stores or a country catalog |
| `find_best_store_for_item` | Rank stores by in-stock quantity (optionally filter by country) |
| `check_cart_availability` | Check whether all items in a shopping list are available at one store |
| `find_best_store_for_cart` | Rank stores by cart fulfillment across multiple items |
## MVP limitations
- Uses unofficial public IKEA APIs — no SLA, may break without notice
- Canada store coverage is complete (15 stores)
- US coverage is incomplete — 4 small-format stores have unknown API IDs (Queens, Alpharetta, Indianapolis, Arlington)
- San Francisco small-format store is intentionally excluded (known ID 3136 returns 405)
- No extra stores are included
- Cash-and-carry availability only — click-and-collect and home delivery not exposed
- HTTP transport is open by default — set `API_KEY` env var to require `x-api-key` header on `/mcp`
- Read-only — no cart, order, or account operations
## Tools
### `search_products`
Search IKEA products by keyword.
**Input**
| param | type | default | required |
|---|---|---|---|
| `query` | string | — | yes |
| `countryCode` | string | `"US"` | no |
| `langCode` | string | `"en"` | no |
| `size` | number | `10` | no |
**Output**
```json
{
"total": 97,
"items": [
{
"itemNo": "20522046",
"name": "BILLY",
"typeName": "Bookcase",
"salesPrice": { "amount": 69.99, "currencyCode": "USD" },
"pipUrl": "https://www.ikea.com/us/en/p/...",
"ratingValue": 4.8,
"ratingCount": 1234
}
]
}
```
---
### `get_product_details`
Get details for a single IKEA product by item number.
**Input**
| param | type | default | required |
|---|---|---|---|
| `itemNo` | string | — | yes |
| `countryCode` | string | `"US"` | no |
| `langCode` | string | `"en"` | no |
**Output**
```json
{
"itemNo": "20522046",
"name": "BILLY",
"typeName": "Bookcase",
"salesPrice": { "amount": 79, "currencyCode": "USD" },
"pipUrl": "https://www.ikea.com/us/en/p/billy-bookcase-white-20522046/",
"designText": "white",
"measureText": "31 1/2x11x79 1/2 \"",
"ratingValue": 4.6,
"ratingCount": 2620
}
```
> `shortDescription` and `materials` are not available from the underlying API.
---
### `check_store_stock`
Check stock at a single IKEA store.
**Input**
| param | type | default | required |
|---|---|---|---|
| `itemNo` | string | — | yes |
| `storeId` | string | — | yes |
| `countryCode` | string | `"US"` | no |
**Output**
```json
{
"storeId": "399",
"availableForCashCarry": true,
"quantity": 110,
"messageType": "HIGH_IN_STOCK",
"errors": null
}
```
On error (e.g. item not carried):
```json
{
"storeId": "026",
"availableForCashCarry": false,
"quantity": null,
"messageType": null,
"errors": [{ "code": 404, "message": "Not found", "meaning": "item not stocked at this store" }]
}
```
---
### `compare_store_stock`
Compare 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.
**Input**
| param | type | default | required |
|---|---|---|---|
| `itemNo` | string | — | yes |
| `storeIds` | string[] (min 2) | — | one of `storeIds`/`countryCode` |
| `countryCode` | `"US"` \| `"CA"` | — | one of `storeIds`/`countryCode` |
| `sortBy` | `"quantity"` \| `"storeId"` | — | no |
`storeIds` takes precedence — if both are provided, `countryCode` only sets the IKEA API locale.
`sortBy: "quantity"` sorts descending, null quantities last, `storeId` as tie-breaker. `sortBy: "storeId"` sorts ascending. Omitting `sortBy` preserves input order.
**Examples**
```json
{ "itemNo": "20522046", "storeIds": ["399", "026", "921"] }
```
```json
{ "itemNo": "20522046", "countryCode": "CA" }
```
**Output** — array of the same shape as `check_store_stock` (one entry per store).
**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.
---
### `check_multi_item_stock`
Check cash-and-carry stock for multiple items at a single store in one call.
**Input**
| param | type | default | required |
|---|---|---|---|
| `storeId` | string | — | yes |
| `itemNos` | string[] (min 1, max 20) | — | yes |
**Output** — array of per-item stock entries in the same order as `itemNos`:
```json
[
{
"itemNo": "20522046",
"storeId": "399",
"storeLabel": "399 (Burbank, CA)",
"availableForCashCarry": true,
"quantity": 104,
"messageType": "HIGH_IN_STOCK",
"errors": []
}
]
```
Items 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.
---
### `find_best_store_for_item`
Find 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.
**Input**
| param | type | default | required |
|---|---|---|---|
| `itemNo` | string | — | yes |
| `storeIds` | string[] | all known stores | no |
| `maxResults` | number | `3` (max 50) | no |
| `countryCode` | `"US"` \| `"CA"` | — | no |
| `minQuantity` | number (int ≥ 1) | — | no |
`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.
**Output** — array of matching stores, up to `maxResults`:
```json
[
{
"storeId": "399",
"storeLabel": "399 (Burbank, CA)",
"availableForCashCarry": true,
"quantity": 104,
"messageType": "HIGH_IN_STOCK"
}
]
```
Returns `[]` if no store has the item in stock. "All known stores" means the ~65 US and Canada entries in `src/data/stores.ts`.
**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.
---
### `check_cart_availability`
Check whether all items in a shopping list are available in sufficient quantity at a single IKEA store.
**Input**
| param | type | default | required |
|---|---|---|---|
| `storeId` | string | — | yes |
| `items` | array of `{ itemNo, quantity }` | — | yes |
| `items[].itemNo` | string | — | yes |
| `items[].quantity` | number | `1` | no |
**Output**
```json
{
"storeId": "399",
"storeLabel": "399 (Burbank, CA)",
"allSufficient": true,
"items": [
{
"itemNo": "20522046",
"quantity": 2,
"inStock": 42,
"sufficient": true,
"eligibleForStockNotification": false,
"errors": []
}
]
}
```
`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.
---
### `find_best_store_for_cart`
Find 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`.
**Input**
| param | type | default | required |
|---|---|---|---|
| `items` | array of `{ itemNo, quantity }` | — | yes |
| `items[].itemNo` | string | — | yes |
| `items[].quantity` | number | `1` | no |
| `storeIds` | string[] | — | no |
| `countryCode` | `"US"` \| `"CA"` | — | no |
| `maxResults` | number | `3` (max 50) | no |
`storeIds` takes precedence. If only `countryCode` is given, searches all catalog stores for that country. If neither is given, searches all ~65 known stores.
**Output** — array of stores ranked by cart fulfillment, up to `maxResults`:
```json
[
{
"storeId": "399",
"storeLabel": "399 (Burbank, CA)",
"allSufficient": true,
"fulfilledCount": 3,
"totalCount": 3,
"items": [
{ "itemNo": "20522046", "quantity": 2, "inStock": 42, "sufficient": true },
{ "itemNo": "40477340", "quantity": 1, "inStock": 5, "sufficient": true },
{ "itemNo": "89268919", "quantity": 1, "inStock": 12, "sufficient": true }
]
}
]
```
`fulfilledCount` = number of items with `sufficient: true`. Sorting: `fulfilledCount` desc → total stock desc → `storeId` asc. Stores with invalid IDs (405) are excluded.
---
## Example workflows
### 1. Search → inspect → check one store
```
1. search_products { "query": "BILLY bookcase" }
→ pick itemNo from results, e.g. "20522046"
2. get_product_details { "itemNo": "20522046" }
→ confirms name, price, dimensions before checking stock
3. check_store_stock { "itemNo": "20522046", "storeId": "399" }
→ { "availableForCashCarry": true, "quantity": 95, "messageType": "HIGH_IN_STOCK" }
```
### 2. Shopping list at one store
Check whether several items are available in a single trip:
```json
{
"tool": "check_multi_item_stock",
"storeId": "399",
"itemNos": ["20522046", "40477340", "89268919"]
}
```
Returns one entry per item in the same order — items not stocked appear with `availableForCashCarry: false` and a 404 error.
### 3. Best store from a mixed US + Canada subset
```json
{
"tool": "find_best_store_for_item",
"itemNo": "20522046",
"storeIds": ["399", "039", "216", "149", "026"],
"maxResults": 3
}
```
Returns the top 3 stores by in-stock quantity across the mixed US/Canada subset. Omit `storeIds` to search all ~65 known stores.
### 4. Best store for a shopping list
Find which store can fulfill the most items from a multi-item cart:
```json
{
"tool": "find_best_store_for_cart",
"items": [
{ "itemNo": "20522046", "quantity": 2 },
{ "itemNo": "40477340", "quantity": 1 },
{ "itemNo": "89268919", "quantity": 1 }
],
"countryCode": "CA",
"maxResults": 3
}
```
Returns 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.
---
## Build and test
```bash
npm install
npm run build # tsc → dist/
npm run typecheck # type-check without emit
npm test # unit tests
node smoke.mjs # end-to-end stdio smoke test
```
`smoke.mjs` exercises all 4 tools against the live IKEA API and prints pass/fail lines to stdout.
## Transports
**stdio** (default — for Claude Desktop / MCP CLI):
```bash
npx ikea-mcp # after npm install (uses bin entry)
node dist/index.js # after local build
npm run dev # dev (tsx, no build needed)
```
**Streamable HTTP** (for remote / network clients):
```bash
node dist/http.js # listens on http://localhost:3000/mcp
PORT=8080 node dist/http.js
# or during dev:
npm run dev:http
```
Requests must include `Accept: application/json, text/event-stream`. Stateless — no session management.
## Deploy (HTTP transport)
Tested target: **Railway** (also works on Render, Heroku, or any Procfile-aware host).
```bash
# 1. build
npm install && npm run build
# 2. run (Procfile: web: node dist/http.js)
# PORT is set automatically by the host
node dist/http.js
```
The `Procfile` in the repo root declares `web: node dist/http.js`. `PORT` is read from the environment (default `3000`). No other env vars required.
Endpoints after deploy:
- `POST /mcp` — MCP Streamable HTTP (requires `Accept: application/json, text/event-stream`)
- `GET /health` — returns `{"status":"ok"}`
> **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.
>
> ```bash
> API_KEY=your-secret node dist/http.js
> ```
## Connecting a local MCP client (stdio)
**Claude Desktop** (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"ikea-mcp": {
"command": "npx",
"args": ["-y", "ikea-mcp"]
}
}
}
```
---
## Connecting a remote MCP client (HTTP)
Point your MCP client at `https://<your-host>/mcp`.
**Claude Desktop** (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"ikea-mcp": {
"type": "http",
"url": "https://<your-host>/mcp"
}
}
}
```
**`.mcp.json`** (project-local, Claude Code):
```json
{
"mcpServers": {
"ikea-mcp": {
"type": "http",
"url": "https://<your-host>/mcp"
}
}
}
```
**Manual / curl** (for debugging):
```bash
curl -X POST https://<your-host>/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```
The `Accept: application/json, text/event-stream` header is required by the MCP SDK — requests without it will be rejected with a `-32000` error.
## Store IDs
Store 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).
Confirmed compatible `storeId` formats:
- Standard 3-digit: `"399"` (Burbank, CA, US), `"216"` (Calgary, AB, CA)
- Leading-zero 3-digit: `"026"` (Canton, MI, US), `"039"` (Montreal, QC, CA)
- 4-digit: `"921"` (Brooklyn, NY, US), `"1129"` (Syracuse, NY, US)
An invalid or unsupported `storeId` returns a 405 error in the `errors` array.
## Limitations
- Uses unofficial public IKEA APIs — no SLA, no auth required, may break without notice.
- Read-only: no cart, no order, no account operations.
- Country-wide fan-out (`countryCode: "US"` ≈ 52 stores, `"CA"` ≈ 15) is capped at 10 concurrent requests and retries once on transient 5xx/network errors.
- Click-and-collect and home-delivery availability are not exposed (cash-and-carry only).
- `size` in `search_products` is capped by IKEA's API (observed max ~24 per page; `total` reflects the full catalogue count).
- US and Canada only — no other countries supported.
## Item numbers
`itemNo` fields accept several formats — all are normalised to 8 digits internally:
| Input | Normalised |
|---|---|
| `"20522046"` | `"20522046"` |
| `"522132"` | `"00522132"` |
| `"005.221.32"` | `"00522132"` |
| `"5-221-32"` | `"00522132"` |
6- 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.
## Supported countries
| Country | Code | Store count |
|---|---|---|
| United States | `US` | ~52 |
| Canada | `CA` | ~15 |
Use `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.
## Rate limits & reliability
- Fan-out requests (country-wide `compare_store_stock` / `find_best_store_for_item`) are capped at **10 concurrent** outbound requests.
- `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).
- `Retry-After` header is respected for 429 responses.
- Do not use in high-frequency loops — the upstream IKEA API has no published rate limit but will block repeated bursts.
## Troubleshooting
| Symptom | Likely cause |
|---|---|
| `405` in `errors` | Invalid `storeId` — use `list_stores` to find valid IDs |
| `404` in `errors` | Item not stocked at that store |
| Empty `find_best_store_for_item` result | No store has the item in stock, or `minQuantity` is too high |
| Slow `countryCode` query | Normal — fan-out to all country stores (capped at 10 concurrent) |
| `itemNo` validation error | Input must resolve to 6–9 digits; see Item numbers above |
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available