An open API service indexing awesome lists of open source software.

https://github.com/taejin5314/ikea-mcp

Read-only IKEA MCP server for product search, store stock lookup, and multi-store stock comparison.
https://github.com/taejin5314/ikea-mcp

claude-code ikea inventory mcp model-context-protocol nodejs typescript

Last synced: 3 months ago
JSON representation

Read-only IKEA MCP server for product search, store stock lookup, and multi-store stock comparison.

Awesome Lists containing this project

README

          

# 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:///mcp`.

**Claude Desktop** (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"ikea-mcp": {
"type": "http",
"url": "https:///mcp"
}
}
}
```

**`.mcp.json`** (project-local, Claude Code):
```json
{
"mcpServers": {
"ikea-mcp": {
"type": "http",
"url": "https:///mcp"
}
}
}
```

**Manual / curl** (for debugging):
```bash
curl -X POST https:///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 |