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.
- Host: GitHub
- URL: https://github.com/taejin5314/ikea-mcp
- Owner: taejin5314
- License: mit
- Created: 2026-03-11T20:18:11.000Z (4 months ago)
- Default Branch: master
- Last Pushed: 2026-03-15T04:25:34.000Z (3 months ago)
- Last Synced: 2026-03-16T19:44:01.436Z (3 months ago)
- Topics: claude-code, ikea, inventory, mcp, model-context-protocol, nodejs, typescript
- Language: TypeScript
- Homepage:
- Size: 109 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
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 |