ikea-mcp 1.6.2 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/README.md +662 -526
- package/dist/data/countries.js +16 -0
- package/dist/data/geo.js +26 -0
- package/dist/data/store-coordinates.js +88 -0
- package/dist/data/stores.js +115 -86
- package/dist/schemas/index.js +51 -13
- package/dist/server.js +53 -16
- package/dist/services/ikea.js +4 -0
- package/dist/tools/check_cart_availability.js +7 -4
- package/dist/tools/check_multi_item_stock.js +2 -1
- package/dist/tools/check_store_stock.js +2 -2
- package/dist/tools/compare_store_stock.js +3 -2
- package/dist/tools/find_best_store_for_cart.js +19 -5
- package/dist/tools/find_best_store_for_item.js +19 -5
- package/dist/tools/find_nearby_stores.js +29 -0
- package/dist/tools/get_product_details.js +2 -2
- package/dist/tools/list_stores.js +11 -16
- package/dist/tools/search_products.js +2 -2
- package/dist/tools/targets.js +16 -0
- package/dist/utils/errors.js +27 -0
- package/dist/utils/http.js +6 -1
- package/package.json +59 -53
package/README.md
CHANGED
|
@@ -1,526 +1,662 @@
|
|
|
1
|
-
# ikea-mcp
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
Check stock at
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
|
101
|
-
|
|
102
|
-
| `
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
{
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1
|
+
# ikea-mcp
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/ikea-mcp)
|
|
4
|
+
[](package.json)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/taejin5314/ikea-mcp/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/taejin5314/ikea-mcp/actions/workflows/live-smoke.yml)
|
|
8
|
+
|
|
9
|
+
An MCP server that lets an AI assistant search IKEA products and check live in-store stock in the US, Canada and Norway.
|
|
10
|
+
Ask "which store near me has everything on this list?" and the assistant answers from IKEA's own stock data.
|
|
11
|
+
|
|
12
|
+
Read-only, no account or API key needed, runs with one `npx` command.
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
Add it to Claude Desktop (`claude_desktop_config.json`), Claude Code (`.mcp.json`) or any MCP client that speaks
|
|
17
|
+
stdio:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"ikea-mcp": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "ikea-mcp"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then ask in plain language:
|
|
31
|
+
|
|
32
|
+
> Find the white BILLY bookcase and tell me which store near Toronto has it in stock.
|
|
33
|
+
|
|
34
|
+
## What it answers
|
|
35
|
+
|
|
36
|
+
These are real responses from the published package against the live IKEA API, captured on 11 November 2026. Each
|
|
37
|
+
call took under 400 ms, including the one that asks all 15 Canadian stores.
|
|
38
|
+
|
|
39
|
+
`search_products` `{ "query": "BILLY bookcase", "countryCode": "CA", "size": 3 }`
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"total": 120,
|
|
44
|
+
"items": [
|
|
45
|
+
{
|
|
46
|
+
"itemNo": "20522046",
|
|
47
|
+
"name": "BILLY",
|
|
48
|
+
"typeName": "Bookcase",
|
|
49
|
+
"salesPrice": { "amount": 90, "currencyCode": "CAD" },
|
|
50
|
+
"pipUrl": "https://www.ikea.com/ca/en/p/billy-bookcase-white-20522046/",
|
|
51
|
+
"ratingValue": 4.5,
|
|
52
|
+
"ratingCount": 2124
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`find_best_store_for_item` `{ "itemNo": "20522046", "countryCode": "CA", "maxResults": 3 }`
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
[
|
|
62
|
+
{ "storeId": "003", "storeLabel": "003 (Richmond, BC, CA)", "quantity": 565, "messageType": "HIGH_IN_STOCK" },
|
|
63
|
+
{ "storeId": "372", "storeLabel": "372 (Vaughan, ON, CA)", "quantity": 111, "messageType": "HIGH_IN_STOCK" },
|
|
64
|
+
{ "storeId": "216", "storeLabel": "216 (Calgary, AB, CA)", "quantity": 94, "messageType": "HIGH_IN_STOCK" }
|
|
65
|
+
]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`find_best_store_for_cart` with two BILLY sizes, ranked by how much of the cart each store can fill:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
[
|
|
72
|
+
{
|
|
73
|
+
"storeId": "003",
|
|
74
|
+
"storeLabel": "003 (Richmond, BC, CA)",
|
|
75
|
+
"allSufficient": true,
|
|
76
|
+
"fulfilledCount": 2,
|
|
77
|
+
"totalCount": 2,
|
|
78
|
+
"items": [
|
|
79
|
+
{ "itemNo": "20522046", "quantity": 2, "inStock": 565, "sufficient": true },
|
|
80
|
+
{ "itemNo": "50522040", "quantity": 1, "inStock": 17, "sufficient": true }
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
(Output shortened: the first shows one of three items and omits `imageUrl`, the second omits
|
|
87
|
+
`availableForCashCarry`, the third shows one of two stores.)
|
|
88
|
+
|
|
89
|
+
## Tools
|
|
90
|
+
|
|
91
|
+
| Tool | What it does |
|
|
92
|
+
| -------------------------- | ---------------------------------------------------------------------- |
|
|
93
|
+
| `search_products` | Search IKEA products by keyword |
|
|
94
|
+
| `get_product_details` | Get details for a single product by item number |
|
|
95
|
+
| `list_stores` | List known store IDs and labels, optionally filtered by country |
|
|
96
|
+
| `find_nearby_stores` | Find the stores closest to a place, with the distance in km |
|
|
97
|
+
| `check_store_stock` | Check cash-and-carry stock at one store |
|
|
98
|
+
| `check_multi_item_stock` | Check stock for multiple items at one store |
|
|
99
|
+
| `compare_store_stock` | Compare stock across explicit stores or a whole country |
|
|
100
|
+
| `find_best_store_for_item` | Rank stores by in-stock quantity, or the nearest stores that have it |
|
|
101
|
+
| `check_cart_availability` | Check whether every item in a shopping list is available at one store |
|
|
102
|
+
| `find_best_store_for_cart` | Rank stores by how much of a multi-item cart they can fill |
|
|
103
|
+
|
|
104
|
+
## Finding stores by place
|
|
105
|
+
|
|
106
|
+
Nobody knows IKEA store IDs, so the ranking tools take a place. Pass `near` with the latitude and longitude of the
|
|
107
|
+
user's city (an assistant can supply an approximate city-centre coordinate) and they search only the closest stores
|
|
108
|
+
and answer nearest first:
|
|
109
|
+
|
|
110
|
+
`find_best_store_for_item` `{ "itemNo": "20522046", "near": { "latitude": 43.65, "longitude": -79.38 } }`
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
[
|
|
114
|
+
{ "storeId": "256", "storeLabel": "256 (Etobicoke, ON, CA)", "quantity": 71, "messageType": "HIGH_IN_STOCK", "distanceKm": 12.9 },
|
|
115
|
+
{ "storeId": "149", "storeLabel": "149 (North York, ON, CA)", "quantity": 59, "messageType": "HIGH_IN_STOCK", "distanceKm": 13.1 },
|
|
116
|
+
{ "storeId": "372", "storeLabel": "372 (Vaughan, ON, CA)", "quantity": 107, "messageType": "HIGH_IN_STOCK", "distanceKm": 19.5 }
|
|
117
|
+
]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
(Captured against the live API on 11 November 2026; `availableForCashCarry` omitted.)
|
|
121
|
+
|
|
122
|
+
- `near` searches the 5 closest stores by default; set `nearestStores` (1–20) to widen it. Add `countryCode` to stay
|
|
123
|
+
inside one country near a border.
|
|
124
|
+
- `maxDistanceKm` (a number greater than 0), used together with `near`, leaves out stores farther than that
|
|
125
|
+
straight-line distance, as in `{ "itemNo": "20522046", "near": { "latitude": 43.65, "longitude": -79.38 }, "maxDistanceKm": 100 }`.
|
|
126
|
+
If no store is within it the tools return `[]` and send no request to IKEA. It is ignored when explicit `storeIds`
|
|
127
|
+
are given; without `near` and without `storeIds` the call is rejected with `maxDistanceKm needs near`.
|
|
128
|
+
- Which stores are searched: explicit `storeIds`, else the stores closest to `near`, else every store in
|
|
129
|
+
`countryCode`, else the whole catalog.
|
|
130
|
+
- `find_nearby_stores` returns just the stores and distances, without a stock lookup.
|
|
131
|
+
- Distances are straight-line. Coordinates come from IKEA's public store lists and are generated by
|
|
132
|
+
`scripts/update-store-coordinates.mjs`; a store without a known coordinate is left out of distance searches.
|
|
133
|
+
|
|
134
|
+
## How it is built
|
|
135
|
+
|
|
136
|
+
- **Two transports from one server.** stdio for local clients and stateless Streamable HTTP for remote ones, with
|
|
137
|
+
optional `x-api-key` auth on `/mcp` and an open `/health` endpoint.
|
|
138
|
+
- **Fan-out with limits.** A country-wide query asks every store in parallel, capped at 10 concurrent requests,
|
|
139
|
+
with one retry on 5xx, 429 or network errors and `Retry-After` respected.
|
|
140
|
+
- **Errors that mean something.** A 404 is "not stocked here", a 405 is "not a valid store" and `inRange: false`
|
|
141
|
+
is "not sold in this market"; each comes back as data on the row instead of failing the whole call, so an
|
|
142
|
+
assistant can explain a partial answer.
|
|
143
|
+
- **Failures the assistant can read.** Invalid arguments and upstream failures come back as tool results marked
|
|
144
|
+
`isError`, naming the field to fix, instead of protocol errors a client may hide. Every request to IKEA times out
|
|
145
|
+
after 10 seconds.
|
|
146
|
+
- **Declared read-only.** Every tool carries MCP annotations (`readOnlyHint`, a title), so clients know they
|
|
147
|
+
change nothing.
|
|
148
|
+
- **Watched daily.** A scheduled job runs the smoke test against the live API and opens an issue when IKEA changes
|
|
149
|
+
something. A weekly job compares the store catalog with IKEA's own store lists and opens an issue when a store
|
|
150
|
+
opens or closes.
|
|
151
|
+
- **Forgiving input.** Item numbers are accepted as `20522046`, `005.221.32` or `5-221-32` and normalised.
|
|
152
|
+
- **Small surface.** Two runtime dependencies (the MCP SDK and zod), inputs validated with zod schemas, unit tests
|
|
153
|
+
on the logic and smoke tests over both transports against the live API.
|
|
154
|
+
|
|
155
|
+
## Limitations
|
|
156
|
+
|
|
157
|
+
- Uses unofficial public IKEA APIs: no SLA, and it may break without notice.
|
|
158
|
+
- US, Canada and Norway only: 60 US stores, 16 in Canada and 7 in Norway, which is every store and small-format
|
|
159
|
+
store IKEA lists as selling cash-and-carry in those countries. Plan-and-order points are excluded because they
|
|
160
|
+
carry no stock, and so is the Østfold small store in Norway, whose stock the API does not report.
|
|
161
|
+
- Cash-and-carry availability only: click-and-collect and home delivery are not exposed.
|
|
162
|
+
- Read-only: no cart, order or account operations.
|
|
163
|
+
- `size` in `search_products` is capped by IKEA's API (observed maximum about 24 per page; `total` is the full
|
|
164
|
+
catalogue count).
|
|
165
|
+
- The HTTP transport is open by default. Set the `API_KEY` environment variable to require an `x-api-key` header
|
|
166
|
+
on `/mcp`.
|
|
167
|
+
|
|
168
|
+
## Tool reference
|
|
169
|
+
|
|
170
|
+
### `search_products`
|
|
171
|
+
|
|
172
|
+
Search IKEA products by keyword.
|
|
173
|
+
|
|
174
|
+
**Input**
|
|
175
|
+
| param | type | default | required |
|
|
176
|
+
|---|---|---|---|
|
|
177
|
+
| `query` | string | — | yes |
|
|
178
|
+
| `countryCode` | string | `"US"` | no |
|
|
179
|
+
| `langCode` | string | country default: `"en"` for US/CA, `"no"` for NO | no |
|
|
180
|
+
| `size` | number | `10` | no |
|
|
181
|
+
|
|
182
|
+
**Output**
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"total": 97,
|
|
186
|
+
"items": [
|
|
187
|
+
{
|
|
188
|
+
"itemNo": "20522046",
|
|
189
|
+
"name": "BILLY",
|
|
190
|
+
"typeName": "Bookcase",
|
|
191
|
+
"salesPrice": { "amount": 69.99, "currencyCode": "USD" },
|
|
192
|
+
"pipUrl": "https://www.ikea.com/us/en/p/...",
|
|
193
|
+
"ratingValue": 4.8,
|
|
194
|
+
"ratingCount": 1234
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
### `get_product_details`
|
|
203
|
+
|
|
204
|
+
Get details for a single IKEA product by item number.
|
|
205
|
+
|
|
206
|
+
**Input**
|
|
207
|
+
| param | type | default | required |
|
|
208
|
+
|---|---|---|---|
|
|
209
|
+
| `itemNo` | string | — | yes |
|
|
210
|
+
| `countryCode` | string | `"US"` | no |
|
|
211
|
+
| `langCode` | string | country default: `"en"` for US/CA, `"no"` for NO | no |
|
|
212
|
+
|
|
213
|
+
**Output**
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"itemNo": "20522046",
|
|
217
|
+
"name": "BILLY",
|
|
218
|
+
"typeName": "Bookcase",
|
|
219
|
+
"salesPrice": { "amount": 79, "currencyCode": "USD" },
|
|
220
|
+
"pipUrl": "https://www.ikea.com/us/en/p/billy-bookcase-white-20522046/",
|
|
221
|
+
"designText": "white",
|
|
222
|
+
"measureText": "31 1/2x11x79 1/2 \"",
|
|
223
|
+
"ratingValue": 4.6,
|
|
224
|
+
"ratingCount": 2620
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
> `shortDescription` and `materials` are not available from the underlying API.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
### `check_store_stock`
|
|
233
|
+
|
|
234
|
+
Check stock at a single IKEA store.
|
|
235
|
+
|
|
236
|
+
**Input**
|
|
237
|
+
| param | type | default | required |
|
|
238
|
+
|---|---|---|---|
|
|
239
|
+
| `itemNo` | string | — | yes |
|
|
240
|
+
| `storeId` | string | — | yes |
|
|
241
|
+
| `countryCode` | string | `"US"` | no |
|
|
242
|
+
|
|
243
|
+
**Output**
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"storeId": "399",
|
|
247
|
+
"availableForCashCarry": true,
|
|
248
|
+
"inRange": true,
|
|
249
|
+
"quantity": 110,
|
|
250
|
+
"messageType": "HIGH_IN_STOCK",
|
|
251
|
+
"errors": []
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
On error (e.g. item not carried):
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"storeId": "026",
|
|
259
|
+
"availableForCashCarry": false,
|
|
260
|
+
"inRange": null,
|
|
261
|
+
"quantity": null,
|
|
262
|
+
"messageType": null,
|
|
263
|
+
"errors": [{ "code": 404, "message": "Not found", "meaning": "item not stocked at this store" }]
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`inRange: false` with an empty `errors` array means the item is not sold at that store or in that country's market (for example a US item number checked against a Norwegian store). Search with the matching `countryCode` to get a local item number.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### `compare_store_stock`
|
|
272
|
+
|
|
273
|
+
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.
|
|
274
|
+
|
|
275
|
+
**Input**
|
|
276
|
+
| param | type | default | required |
|
|
277
|
+
|---|---|---|---|
|
|
278
|
+
| `itemNo` | string | — | yes |
|
|
279
|
+
| `storeIds` | string[] (min 2) | — | one of `storeIds`/`countryCode` |
|
|
280
|
+
| `countryCode` | `"US"` \| `"CA"` \| `"NO"` | — | one of `storeIds`/`countryCode` |
|
|
281
|
+
| `sortBy` | `"quantity"` \| `"storeId"` | — | no |
|
|
282
|
+
|
|
283
|
+
`storeIds` takes precedence — if both are provided, `countryCode` only sets the IKEA API locale.
|
|
284
|
+
`sortBy: "quantity"` sorts descending, null quantities last, `storeId` as tie-breaker. `sortBy: "storeId"` sorts ascending. Omitting `sortBy` preserves input order.
|
|
285
|
+
|
|
286
|
+
**Examples**
|
|
287
|
+
```json
|
|
288
|
+
{ "itemNo": "20522046", "storeIds": ["399", "026", "921"] }
|
|
289
|
+
```
|
|
290
|
+
```json
|
|
291
|
+
{ "itemNo": "20522046", "countryCode": "CA" }
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Output** — array of the same shape as `check_store_stock` (one entry per store).
|
|
295
|
+
|
|
296
|
+
**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.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
### `check_multi_item_stock`
|
|
301
|
+
|
|
302
|
+
Check cash-and-carry stock for multiple items at a single store in one call.
|
|
303
|
+
|
|
304
|
+
**Input**
|
|
305
|
+
| param | type | default | required |
|
|
306
|
+
|---|---|---|---|
|
|
307
|
+
| `storeId` | string | — | yes |
|
|
308
|
+
| `itemNos` | string[] (min 1, max 20) | — | yes |
|
|
309
|
+
|
|
310
|
+
**Output** — array of per-item stock entries in the same order as `itemNos`:
|
|
311
|
+
|
|
312
|
+
```json
|
|
313
|
+
[
|
|
314
|
+
{
|
|
315
|
+
"itemNo": "20522046",
|
|
316
|
+
"storeId": "399",
|
|
317
|
+
"storeLabel": "399 (Burbank, CA)",
|
|
318
|
+
"availableForCashCarry": true,
|
|
319
|
+
"inRange": true,
|
|
320
|
+
"quantity": 104,
|
|
321
|
+
"messageType": "HIGH_IN_STOCK",
|
|
322
|
+
"errors": []
|
|
323
|
+
}
|
|
324
|
+
]
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
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.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
### `find_best_store_for_item`
|
|
332
|
+
|
|
333
|
+
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.
|
|
334
|
+
|
|
335
|
+
**Input**
|
|
336
|
+
| param | type | default | required |
|
|
337
|
+
|---|---|---|---|
|
|
338
|
+
| `itemNo` | string | — | yes |
|
|
339
|
+
| `storeIds` | string[] | all known stores | no |
|
|
340
|
+
| `maxResults` | number | `3` (max 50) | no |
|
|
341
|
+
| `countryCode` | `"US"` \| `"CA"` \| `"NO"` | — | no |
|
|
342
|
+
| `minQuantity` | number (int ≥ 1) | — | no |
|
|
343
|
+
| `maxDistanceKm` | number (> 0) | — | no |
|
|
344
|
+
|
|
345
|
+
`storeIds` takes precedence. If only `countryCode` is given, searches all catalog stores for that country. If neither is given, searches all 83 known stores. `minQuantity` excludes stores with quantity below the threshold. `maxDistanceKm` applies only with `near` (see "Finding stores by place").
|
|
346
|
+
|
|
347
|
+
**Output** — array of matching stores, up to `maxResults`:
|
|
348
|
+
```json
|
|
349
|
+
[
|
|
350
|
+
{
|
|
351
|
+
"storeId": "399",
|
|
352
|
+
"storeLabel": "399 (Burbank, CA)",
|
|
353
|
+
"availableForCashCarry": true,
|
|
354
|
+
"quantity": 104,
|
|
355
|
+
"messageType": "HIGH_IN_STOCK"
|
|
356
|
+
}
|
|
357
|
+
]
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Returns `[]` if no store has the item in stock. "All known stores" means the 83 US, Canada and Norway entries in `src/data/stores.ts`.
|
|
361
|
+
|
|
362
|
+
**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.
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
### `check_cart_availability`
|
|
367
|
+
|
|
368
|
+
Check whether all items in a shopping list are available in sufficient quantity at a single IKEA store.
|
|
369
|
+
|
|
370
|
+
**Input**
|
|
371
|
+
| param | type | default | required |
|
|
372
|
+
|---|---|---|---|
|
|
373
|
+
| `storeId` | string | — | yes |
|
|
374
|
+
| `items` | array of `{ itemNo, quantity }` | — | yes |
|
|
375
|
+
| `items[].itemNo` | string | — | yes |
|
|
376
|
+
| `items[].quantity` | number | `1` | no |
|
|
377
|
+
|
|
378
|
+
**Output**
|
|
379
|
+
```json
|
|
380
|
+
{
|
|
381
|
+
"storeId": "399",
|
|
382
|
+
"storeLabel": "399 (Burbank, CA)",
|
|
383
|
+
"allSufficient": true,
|
|
384
|
+
"items": [
|
|
385
|
+
{
|
|
386
|
+
"itemNo": "20522046",
|
|
387
|
+
"quantity": 2,
|
|
388
|
+
"inStock": 42,
|
|
389
|
+
"inRange": true,
|
|
390
|
+
"sufficient": true,
|
|
391
|
+
"eligibleForStockNotification": false,
|
|
392
|
+
"errors": []
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
`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.
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
### `find_best_store_for_cart`
|
|
403
|
+
|
|
404
|
+
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`.
|
|
405
|
+
|
|
406
|
+
**Input**
|
|
407
|
+
| param | type | default | required |
|
|
408
|
+
|---|---|---|---|
|
|
409
|
+
| `items` | array of `{ itemNo, quantity }` | — | yes |
|
|
410
|
+
| `items[].itemNo` | string | — | yes |
|
|
411
|
+
| `items[].quantity` | number | `1` | no |
|
|
412
|
+
| `storeIds` | string[] | — | no |
|
|
413
|
+
| `countryCode` | `"US"` \| `"CA"` \| `"NO"` | — | no |
|
|
414
|
+
| `maxResults` | number | `3` (max 50) | no |
|
|
415
|
+
| `maxDistanceKm` | number (> 0) | — | no |
|
|
416
|
+
|
|
417
|
+
`storeIds` takes precedence. If only `countryCode` is given, searches all catalog stores for that country. If neither is given, searches all 83 known stores. `maxDistanceKm` applies only with `near` (see "Finding stores by place").
|
|
418
|
+
|
|
419
|
+
**Output** — array of stores ranked by cart fulfillment, up to `maxResults`:
|
|
420
|
+
```json
|
|
421
|
+
[
|
|
422
|
+
{
|
|
423
|
+
"storeId": "399",
|
|
424
|
+
"storeLabel": "399 (Burbank, CA)",
|
|
425
|
+
"allSufficient": true,
|
|
426
|
+
"fulfilledCount": 3,
|
|
427
|
+
"totalCount": 3,
|
|
428
|
+
"items": [
|
|
429
|
+
{ "itemNo": "20522046", "quantity": 2, "inStock": 42, "sufficient": true },
|
|
430
|
+
{ "itemNo": "40477340", "quantity": 1, "inStock": 5, "sufficient": true },
|
|
431
|
+
{ "itemNo": "89268919", "quantity": 1, "inStock": 12, "sufficient": true }
|
|
432
|
+
]
|
|
433
|
+
}
|
|
434
|
+
]
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
`fulfilledCount` = number of items with `sufficient: true`. Sorting: `fulfilledCount` desc → total stock desc → `storeId` asc. Stores with invalid IDs (405) are excluded.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Example workflows
|
|
442
|
+
|
|
443
|
+
### 1. Search → inspect → check one store
|
|
444
|
+
|
|
445
|
+
```
|
|
446
|
+
1. search_products { "query": "BILLY bookcase" }
|
|
447
|
+
→ pick itemNo from results, e.g. "20522046"
|
|
448
|
+
|
|
449
|
+
2. get_product_details { "itemNo": "20522046" }
|
|
450
|
+
→ confirms name, price, dimensions before checking stock
|
|
451
|
+
|
|
452
|
+
3. check_store_stock { "itemNo": "20522046", "storeId": "399" }
|
|
453
|
+
→ { "availableForCashCarry": true, "quantity": 95, "messageType": "HIGH_IN_STOCK" }
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### 2. Shopping list at one store
|
|
457
|
+
|
|
458
|
+
Check whether several items are available in a single trip:
|
|
459
|
+
|
|
460
|
+
```json
|
|
461
|
+
{
|
|
462
|
+
"tool": "check_multi_item_stock",
|
|
463
|
+
"storeId": "399",
|
|
464
|
+
"itemNos": ["20522046", "40477340", "89268919"]
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Returns one entry per item in the same order — items not stocked appear with `availableForCashCarry: false` and a 404 error.
|
|
469
|
+
|
|
470
|
+
### 3. Best store from a mixed US + Canada subset
|
|
471
|
+
|
|
472
|
+
```json
|
|
473
|
+
{
|
|
474
|
+
"tool": "find_best_store_for_item",
|
|
475
|
+
"itemNo": "20522046",
|
|
476
|
+
"storeIds": ["399", "039", "216", "149", "026"],
|
|
477
|
+
"maxResults": 3
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
Returns the top 3 stores by in-stock quantity across the mixed US/Canada subset. Omit `storeIds` to search all 83 known stores.
|
|
482
|
+
|
|
483
|
+
### 4. Best store for a shopping list
|
|
484
|
+
|
|
485
|
+
Find which store can fulfill the most items from a multi-item cart:
|
|
486
|
+
|
|
487
|
+
```json
|
|
488
|
+
{
|
|
489
|
+
"tool": "find_best_store_for_cart",
|
|
490
|
+
"items": [
|
|
491
|
+
{ "itemNo": "20522046", "quantity": 2 },
|
|
492
|
+
{ "itemNo": "40477340", "quantity": 1 },
|
|
493
|
+
{ "itemNo": "89268919", "quantity": 1 }
|
|
494
|
+
],
|
|
495
|
+
"countryCode": "CA",
|
|
496
|
+
"maxResults": 3
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
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.
|
|
501
|
+
|
|
502
|
+
### 5. Norway: search in Norwegian, then compare every Norwegian store
|
|
503
|
+
|
|
504
|
+
```
|
|
505
|
+
1. search_products { "query": "billy bokhylle", "countryCode": "NO" }
|
|
506
|
+
→ Norwegian names and NOK prices; langCode defaults to "no"
|
|
507
|
+
|
|
508
|
+
2. compare_store_stock { "itemNo": "00263850", "countryCode": "NO" }
|
|
509
|
+
→ one row per Norwegian store, labelled like "095 (Furuset, Oslo, NO)"
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## Build and test
|
|
515
|
+
|
|
516
|
+
```bash
|
|
517
|
+
npm install
|
|
518
|
+
npm run build # tsc → dist/
|
|
519
|
+
npm run typecheck # type-check without emit
|
|
520
|
+
npm test # unit tests
|
|
521
|
+
node smoke.mjs # end-to-end stdio smoke test
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
`smoke.mjs` (`npm run smoke`) drives the built server over stdio against the live IKEA API, including five Norway steps, and prints pass/fail lines to stdout; `smoke-http.mjs` does the same over HTTP.
|
|
525
|
+
|
|
526
|
+
## Transports
|
|
527
|
+
|
|
528
|
+
**stdio** (default — for Claude Desktop / MCP CLI):
|
|
529
|
+
```bash
|
|
530
|
+
npx ikea-mcp # after npm install (uses bin entry)
|
|
531
|
+
node dist/index.js # after local build
|
|
532
|
+
npm run dev # dev (tsx, no build needed)
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
**Streamable HTTP** (for remote / network clients):
|
|
536
|
+
```bash
|
|
537
|
+
node dist/http.js # listens on http://localhost:3000/mcp
|
|
538
|
+
PORT=8080 node dist/http.js
|
|
539
|
+
# or during dev:
|
|
540
|
+
npm run dev:http
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
Requests must include `Accept: application/json, text/event-stream`. Stateless — no session management.
|
|
544
|
+
|
|
545
|
+
## Deploy (HTTP transport)
|
|
546
|
+
|
|
547
|
+
Tested target: **Railway** (also works on Render, Heroku, or any Procfile-aware host).
|
|
548
|
+
|
|
549
|
+
```bash
|
|
550
|
+
# 1. build
|
|
551
|
+
npm install && npm run build
|
|
552
|
+
|
|
553
|
+
# 2. run (Procfile: web: node dist/http.js)
|
|
554
|
+
# PORT is set automatically by the host
|
|
555
|
+
node dist/http.js
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
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.
|
|
559
|
+
|
|
560
|
+
Endpoints after deploy:
|
|
561
|
+
- `POST /mcp` — MCP Streamable HTTP (requires `Accept: application/json, text/event-stream`)
|
|
562
|
+
- `GET /health` — returns `{"status":"ok"}`
|
|
563
|
+
|
|
564
|
+
> **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.
|
|
565
|
+
>
|
|
566
|
+
> ```bash
|
|
567
|
+
> API_KEY=your-secret node dist/http.js
|
|
568
|
+
> ```
|
|
569
|
+
|
|
570
|
+
## Connecting a remote MCP client (HTTP)
|
|
571
|
+
|
|
572
|
+
Point your MCP client at `https://<your-host>/mcp`.
|
|
573
|
+
|
|
574
|
+
**Claude Desktop** (`claude_desktop_config.json`):
|
|
575
|
+
```json
|
|
576
|
+
{
|
|
577
|
+
"mcpServers": {
|
|
578
|
+
"ikea-mcp": {
|
|
579
|
+
"type": "http",
|
|
580
|
+
"url": "https://<your-host>/mcp"
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
**`.mcp.json`** (project-local, Claude Code):
|
|
587
|
+
```json
|
|
588
|
+
{
|
|
589
|
+
"mcpServers": {
|
|
590
|
+
"ikea-mcp": {
|
|
591
|
+
"type": "http",
|
|
592
|
+
"url": "https://<your-host>/mcp"
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
**Manual / curl** (for debugging):
|
|
599
|
+
```bash
|
|
600
|
+
curl -X POST https://<your-host>/mcp \
|
|
601
|
+
-H "Content-Type: application/json" \
|
|
602
|
+
-H "Accept: application/json, text/event-stream" \
|
|
603
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
The `Accept: application/json, text/event-stream` header is required by the MCP SDK — requests without it will be rejected with a `-32000` error.
|
|
607
|
+
|
|
608
|
+
## Store IDs
|
|
609
|
+
|
|
610
|
+
Store metadata (ID → city label) lives in `src/data/stores.ts`. 60 US stores confirmed from `ikea.com/us/en/stores/` pages and IKEA's public store list; 16 Canada stores confirmed from `ikea.com/ca/en/stores/` pages (all probed against the stock API). 7 Norway stores confirmed from `ikea.com/no/no/stores/` pages (store code in the page's embedded business-unit JSON), all probed against the stock API.
|
|
611
|
+
|
|
612
|
+
Confirmed compatible `storeId` formats:
|
|
613
|
+
- Standard 3-digit: `"399"` (Burbank, CA, US), `"216"` (Calgary, AB, CA)
|
|
614
|
+
- Leading-zero 3-digit: `"026"` (Canton, MI, US), `"039"` (Montreal, QC, CA)
|
|
615
|
+
- 4-digit: `"921"` (Brooklyn, NY, US), `"1129"` (Syracuse, NY, US)
|
|
616
|
+
|
|
617
|
+
Norwegian IDs use the same 3-digit formats, e.g. `"095"` (Furuset, Oslo, NO) and the leading-zero `"007"` (Sørlandet, Kristiansand, NO).
|
|
618
|
+
|
|
619
|
+
An invalid or unsupported `storeId` returns a 405 error in the `errors` array.
|
|
620
|
+
|
|
621
|
+
## Item numbers
|
|
622
|
+
|
|
623
|
+
`itemNo` fields accept several formats — all are normalised to 8 digits internally:
|
|
624
|
+
|
|
625
|
+
| Input | Normalised |
|
|
626
|
+
|---|---|
|
|
627
|
+
| `"20522046"` | `"20522046"` |
|
|
628
|
+
| `"522132"` | `"00522132"` |
|
|
629
|
+
| `"005.221.32"` | `"00522132"` |
|
|
630
|
+
| `"5-221-32"` | `"00522132"` |
|
|
631
|
+
|
|
632
|
+
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.
|
|
633
|
+
|
|
634
|
+
Item numbers are per market. An item found with `countryCode: "US"` may not exist in the Norwegian range; stock tools then return `inRange: false`. Search with the country you intend to shop in.
|
|
635
|
+
|
|
636
|
+
## Supported countries
|
|
637
|
+
|
|
638
|
+
| Country | Code | Store count |
|
|
639
|
+
|---|---|---|
|
|
640
|
+
| United States | `US` | 60 |
|
|
641
|
+
| Canada | `CA` | 16 |
|
|
642
|
+
| Norway | `NO` | 7 |
|
|
643
|
+
|
|
644
|
+
Use `list_stores` to get the current catalog. Every store in it answered the stock API when it was added, and a weekly job checks the catalog against IKEA's store lists. To add a store or a country, see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
645
|
+
|
|
646
|
+
## Rate limits & reliability
|
|
647
|
+
|
|
648
|
+
- Fan-out requests (country-wide `compare_store_stock` / `find_best_store_for_item`) are capped at **10 concurrent** outbound requests.
|
|
649
|
+
- `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).
|
|
650
|
+
- `Retry-After` header is respected for 429 responses.
|
|
651
|
+
- Do not use in high-frequency loops — the upstream IKEA API has no published rate limit but will block repeated bursts.
|
|
652
|
+
|
|
653
|
+
## Troubleshooting
|
|
654
|
+
|
|
655
|
+
| Symptom | Likely cause |
|
|
656
|
+
|---|---|
|
|
657
|
+
| `405` in `errors` | Invalid `storeId` — use `list_stores` to find valid IDs |
|
|
658
|
+
| `404` in `errors` | Item not stocked at that store |
|
|
659
|
+
| `inRange: false`, empty `errors` | Item not sold at that store or in that market — search with the matching `countryCode` |
|
|
660
|
+
| Empty `find_best_store_for_item` result | No store has the item in stock, or `minQuantity` is too high |
|
|
661
|
+
| Slow `countryCode` query | Normal — fan-out to all country stores (capped at 10 concurrent) |
|
|
662
|
+
| `itemNo` validation error | Input must resolve to 6–9 digits; see Item numbers above |
|