ikea-mcp 1.5.0 → 1.6.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/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Read-only MCP server for IKEA product search and in-store stock lookup.
|
|
4
4
|
|
|
5
|
-
**Transports:** stdio (Claude Desktop / MCP CLI) · Streamable HTTP (remote clients)
|
|
5
|
+
**Transports:** stdio (Claude Desktop / MCP CLI) · Streamable HTTP (remote clients)
|
|
6
6
|
**License:** MIT · **No auth required to run locally**
|
|
7
7
|
|
|
8
8
|
## Capabilities
|
|
@@ -15,10 +15,15 @@ Read-only MCP server for IKEA product search and in-store stock lookup.
|
|
|
15
15
|
| `check_multi_item_stock` | Check stock for multiple items at one store |
|
|
16
16
|
| `compare_store_stock` | Compare stock across explicit stores or a country catalog |
|
|
17
17
|
| `find_best_store_for_item` | Rank stores by in-stock quantity (optionally filter by country) |
|
|
18
|
+
| `check_cart_availability` | Check whether all items in a shopping list are available at one store |
|
|
19
|
+
| `find_best_store_for_cart` | Rank stores by cart fulfillment across multiple items |
|
|
18
20
|
|
|
19
21
|
## MVP limitations
|
|
20
22
|
- Uses unofficial public IKEA APIs — no SLA, may break without notice
|
|
21
|
-
-
|
|
23
|
+
- Canada store coverage is complete (15 stores)
|
|
24
|
+
- US coverage is incomplete — 4 small-format stores have unknown API IDs (Queens, Alpharetta, Indianapolis, Arlington)
|
|
25
|
+
- San Francisco small-format store is intentionally excluded (known ID 3136 returns 405)
|
|
26
|
+
- No extra stores are included
|
|
22
27
|
- Cash-and-carry availability only — click-and-collect and home delivery not exposed
|
|
23
28
|
- HTTP transport is open by default — set `API_KEY` env var to require `x-api-key` header on `/mcp`
|
|
24
29
|
- Read-only — no cart, order, or account operations
|
|
@@ -215,6 +220,79 @@ Returns `[]` if no store has the item in stock. "All known stores" means the ~65
|
|
|
215
220
|
|
|
216
221
|
---
|
|
217
222
|
|
|
223
|
+
### `check_cart_availability`
|
|
224
|
+
|
|
225
|
+
Check whether all items in a shopping list are available in sufficient quantity at a single IKEA store.
|
|
226
|
+
|
|
227
|
+
**Input**
|
|
228
|
+
| param | type | default | required |
|
|
229
|
+
|---|---|---|---|
|
|
230
|
+
| `storeId` | string | — | yes |
|
|
231
|
+
| `items` | array of `{ itemNo, quantity }` | — | yes |
|
|
232
|
+
| `items[].itemNo` | string | — | yes |
|
|
233
|
+
| `items[].quantity` | number | `1` | no |
|
|
234
|
+
|
|
235
|
+
**Output**
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"storeId": "399",
|
|
239
|
+
"storeLabel": "399 (Burbank, CA)",
|
|
240
|
+
"allSufficient": true,
|
|
241
|
+
"items": [
|
|
242
|
+
{
|
|
243
|
+
"itemNo": "20522046",
|
|
244
|
+
"quantity": 2,
|
|
245
|
+
"inStock": 42,
|
|
246
|
+
"sufficient": true,
|
|
247
|
+
"eligibleForStockNotification": false,
|
|
248
|
+
"errors": []
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`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.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
### `find_best_store_for_cart`
|
|
259
|
+
|
|
260
|
+
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`.
|
|
261
|
+
|
|
262
|
+
**Input**
|
|
263
|
+
| param | type | default | required |
|
|
264
|
+
|---|---|---|---|
|
|
265
|
+
| `items` | array of `{ itemNo, quantity }` | — | yes |
|
|
266
|
+
| `items[].itemNo` | string | — | yes |
|
|
267
|
+
| `items[].quantity` | number | `1` | no |
|
|
268
|
+
| `storeIds` | string[] | — | no |
|
|
269
|
+
| `countryCode` | `"US"` \| `"CA"` | — | no |
|
|
270
|
+
| `maxResults` | number | `3` (max 50) | no |
|
|
271
|
+
|
|
272
|
+
`storeIds` takes precedence. If only `countryCode` is given, searches all catalog stores for that country. If neither is given, searches all ~65 known stores.
|
|
273
|
+
|
|
274
|
+
**Output** — array of stores ranked by cart fulfillment, up to `maxResults`:
|
|
275
|
+
```json
|
|
276
|
+
[
|
|
277
|
+
{
|
|
278
|
+
"storeId": "399",
|
|
279
|
+
"storeLabel": "399 (Burbank, CA)",
|
|
280
|
+
"allSufficient": true,
|
|
281
|
+
"fulfilledCount": 3,
|
|
282
|
+
"totalCount": 3,
|
|
283
|
+
"items": [
|
|
284
|
+
{ "itemNo": "20522046", "quantity": 2, "inStock": 42, "sufficient": true },
|
|
285
|
+
{ "itemNo": "40477340", "quantity": 1, "inStock": 5, "sufficient": true },
|
|
286
|
+
{ "itemNo": "89268919", "quantity": 1, "inStock": 12, "sufficient": true }
|
|
287
|
+
]
|
|
288
|
+
}
|
|
289
|
+
]
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`fulfilledCount` = number of items with `sufficient: true`. Sorting: `fulfilledCount` desc → total stock desc → `storeId` asc. Stores with invalid IDs (405) are excluded.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
218
296
|
## Example workflows
|
|
219
297
|
|
|
220
298
|
### 1. Search → inspect → check one store
|
|
@@ -257,6 +335,25 @@ Returns one entry per item in the same order — items not stocked appear with `
|
|
|
257
335
|
|
|
258
336
|
Returns the top 3 stores by in-stock quantity across the mixed US/Canada subset. Omit `storeIds` to search all ~65 known stores.
|
|
259
337
|
|
|
338
|
+
### 4. Best store for a shopping list
|
|
339
|
+
|
|
340
|
+
Find which store can fulfill the most items from a multi-item cart:
|
|
341
|
+
|
|
342
|
+
```json
|
|
343
|
+
{
|
|
344
|
+
"tool": "find_best_store_for_cart",
|
|
345
|
+
"items": [
|
|
346
|
+
{ "itemNo": "20522046", "quantity": 2 },
|
|
347
|
+
{ "itemNo": "40477340", "quantity": 1 },
|
|
348
|
+
{ "itemNo": "89268919", "quantity": 1 }
|
|
349
|
+
],
|
|
350
|
+
"countryCode": "CA",
|
|
351
|
+
"maxResults": 3
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
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.
|
|
356
|
+
|
|
260
357
|
---
|
|
261
358
|
|
|
262
359
|
## Build and test
|
|
@@ -378,8 +475,6 @@ Confirmed compatible `storeId` formats:
|
|
|
378
475
|
- Leading-zero 3-digit: `"026"` (Canton, MI, US), `"039"` (Montreal, QC, CA)
|
|
379
476
|
- 4-digit: `"921"` (Brooklyn, NY, US), `"1129"` (Syracuse, NY, US)
|
|
380
477
|
|
|
381
|
-
Some stores remain unconfirmed (no store page ID found or not yet probed). A few valid-looking IDs from the stock API have no known city mapping — see TODO comments in `stores.ts`.
|
|
382
|
-
|
|
383
478
|
An invalid or unsupported `storeId` returns a 405 error in the `errors` array.
|
|
384
479
|
|
|
385
480
|
## Limitations
|
package/dist/schemas/index.js
CHANGED
|
@@ -76,3 +76,12 @@ export const CheckCartAvailabilityInput = z.object({
|
|
|
76
76
|
quantity: z.number().int().min(1).max(99).default(1),
|
|
77
77
|
})).min(1).max(20),
|
|
78
78
|
});
|
|
79
|
+
export const FindBestStoreForCartInput = z.object({
|
|
80
|
+
items: z.array(z.object({
|
|
81
|
+
itemNo: ItemNo,
|
|
82
|
+
quantity: z.number().int().min(1).max(99).default(1),
|
|
83
|
+
})).min(1).max(20),
|
|
84
|
+
storeIds: z.array(z.string().min(1)).optional(),
|
|
85
|
+
countryCode: CountryCodeEnum.optional(),
|
|
86
|
+
maxResults: z.number().int().min(1).max(50).default(3),
|
|
87
|
+
});
|
package/dist/server.js
CHANGED
|
@@ -8,7 +8,8 @@ import { getProductDetailsTool } from "./tools/get_product_details.js";
|
|
|
8
8
|
import { checkMultiItemStockTool } from "./tools/check_multi_item_stock.js";
|
|
9
9
|
import { listStoresTool } from "./tools/list_stores.js";
|
|
10
10
|
import { checkCartAvailabilityTool } from "./tools/check_cart_availability.js";
|
|
11
|
-
|
|
11
|
+
import { findBestStoreForCartTool } from "./tools/find_best_store_for_cart.js";
|
|
12
|
+
const tools = [listStoresTool, searchProductsTool, checkStoreStockTool, compareStoreStockTool, findBestStoreForItemTool, getProductDetailsTool, checkMultiItemStockTool, checkCartAvailabilityTool, findBestStoreForCartTool];
|
|
12
13
|
export function createServer() {
|
|
13
14
|
const server = new Server({ name: "ikea-mcp", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
14
15
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
package/dist/services/ikea.js
CHANGED
|
@@ -40,6 +40,7 @@ export function projectMultiItemStock(result, itemNos) {
|
|
|
40
40
|
availableForCashCarry: false,
|
|
41
41
|
quantity: null,
|
|
42
42
|
messageType: null,
|
|
43
|
+
eligibleForStockNotification: null,
|
|
43
44
|
errors: [{ ...storeError, meaning: "store ID does not exist" }],
|
|
44
45
|
};
|
|
45
46
|
}
|
|
@@ -50,6 +51,7 @@ export function projectMultiItemStock(result, itemNos) {
|
|
|
50
51
|
availableForCashCarry: a.availableForCashCarry,
|
|
51
52
|
quantity: a.buyingOption.cashCarry.availability.quantity,
|
|
52
53
|
messageType: a.buyingOption.cashCarry.availability.probability.thisDay.messageType,
|
|
54
|
+
eligibleForStockNotification: a.buyingOption.cashCarry.eligibleForStockNotification ?? null,
|
|
53
55
|
errors: [],
|
|
54
56
|
};
|
|
55
57
|
}
|
|
@@ -64,6 +66,7 @@ export function projectMultiItemStock(result, itemNos) {
|
|
|
64
66
|
availableForCashCarry: false,
|
|
65
67
|
quantity: null,
|
|
66
68
|
messageType: null,
|
|
69
|
+
eligibleForStockNotification: null,
|
|
67
70
|
errors: annotatedNotStocked,
|
|
68
71
|
};
|
|
69
72
|
});
|
|
@@ -22,6 +22,7 @@ export const checkMultiItemStockTool = {
|
|
|
22
22
|
availableForCashCarry: row.availableForCashCarry,
|
|
23
23
|
quantity: row.quantity,
|
|
24
24
|
messageType: row.messageType,
|
|
25
|
+
eligibleForStockNotification: row.eligibleForStockNotification,
|
|
25
26
|
errors: row.errors,
|
|
26
27
|
}));
|
|
27
28
|
return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { FindBestStoreForCartInput } from "../schemas/index.js";
|
|
2
|
+
import { getMultiItemStock, projectMultiItemStock } from "../services/ikea.js";
|
|
3
|
+
import { STORE_LABELS, storeLabel, storeIdsByCountry } from "../data/stores.js";
|
|
4
|
+
import { pMap } from "../utils/http.js";
|
|
5
|
+
export const findBestStoreForCartTool = {
|
|
6
|
+
name: "find_best_store_for_cart",
|
|
7
|
+
description: "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 ('US' or 'CA'). Explicit storeIds take precedence over countryCode.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
items: {
|
|
12
|
+
type: "array",
|
|
13
|
+
items: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
itemNo: { type: "string" },
|
|
17
|
+
quantity: { type: "number", default: 1 },
|
|
18
|
+
},
|
|
19
|
+
required: ["itemNo"],
|
|
20
|
+
},
|
|
21
|
+
minItems: 1,
|
|
22
|
+
maxItems: 20,
|
|
23
|
+
},
|
|
24
|
+
storeIds: { type: "array", items: { type: "string" } },
|
|
25
|
+
countryCode: { type: "string", enum: ["US", "CA"] },
|
|
26
|
+
maxResults: { type: "number", default: 3 },
|
|
27
|
+
},
|
|
28
|
+
required: ["items"],
|
|
29
|
+
},
|
|
30
|
+
async handler(rawInput) {
|
|
31
|
+
const input = FindBestStoreForCartInput.parse(rawInput);
|
|
32
|
+
const targetIds = input.storeIds
|
|
33
|
+
?? (input.countryCode ? storeIdsByCountry(input.countryCode) : Object.keys(STORE_LABELS));
|
|
34
|
+
const itemNos = input.items.map((i) => i.itemNo);
|
|
35
|
+
const storeResults = await pMap(targetIds, async (storeId) => {
|
|
36
|
+
const data = await getMultiItemStock(itemNos, storeId);
|
|
37
|
+
const projected = projectMultiItemStock(data, itemNos);
|
|
38
|
+
return { storeId, projected };
|
|
39
|
+
}, 10);
|
|
40
|
+
const scored = storeResults
|
|
41
|
+
.filter((r) => {
|
|
42
|
+
// Exclude stores where a 405 store error hit all items
|
|
43
|
+
const firstErrors = r.projected[0]?.errors ?? [];
|
|
44
|
+
return !firstErrors.some((e) => e.code === 405);
|
|
45
|
+
})
|
|
46
|
+
.map(({ storeId, projected }) => {
|
|
47
|
+
const items = input.items.map((item, i) => {
|
|
48
|
+
const p = projected[i];
|
|
49
|
+
const inStock = p.quantity;
|
|
50
|
+
return {
|
|
51
|
+
itemNo: item.itemNo,
|
|
52
|
+
quantity: item.quantity,
|
|
53
|
+
inStock,
|
|
54
|
+
sufficient: inStock !== null && inStock >= item.quantity,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
const fulfilledCount = items.filter((i) => i.sufficient).length;
|
|
58
|
+
const totalStock = items.reduce((sum, i) => sum + (i.inStock ?? 0), 0);
|
|
59
|
+
return { storeId, fulfilledCount, totalStock, items };
|
|
60
|
+
});
|
|
61
|
+
scored.sort((a, b) => {
|
|
62
|
+
const fc = b.fulfilledCount - a.fulfilledCount;
|
|
63
|
+
if (fc !== 0)
|
|
64
|
+
return fc;
|
|
65
|
+
const ts = b.totalStock - a.totalStock;
|
|
66
|
+
if (ts !== 0)
|
|
67
|
+
return ts;
|
|
68
|
+
return a.storeId.localeCompare(b.storeId);
|
|
69
|
+
});
|
|
70
|
+
const ranked = scored.slice(0, input.maxResults).map(({ storeId, fulfilledCount, totalStock: _ts, items }) => ({
|
|
71
|
+
storeId,
|
|
72
|
+
storeLabel: storeLabel(storeId) ?? storeId,
|
|
73
|
+
allSufficient: fulfilledCount === input.items.length,
|
|
74
|
+
fulfilledCount,
|
|
75
|
+
totalCount: input.items.length,
|
|
76
|
+
items,
|
|
77
|
+
}));
|
|
78
|
+
return { content: [{ type: "text", text: JSON.stringify(ranked, null, 2) }] };
|
|
79
|
+
},
|
|
80
|
+
};
|
package/package.json
CHANGED