ikea-mcp 1.5.0 → 1.6.1

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
- - ~65 confirmed US and Canada stores in catalog (`src/data/stores.ts`); some IDs remain unverified
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
@@ -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
- const tools = [listStoresTool, searchProductsTool, checkStoreStockTool, compareStoreStockTool, findBestStoreForItemTool, getProductDetailsTool, checkMultiItemStockTool, checkCartAvailabilityTool];
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 () => ({
@@ -14,9 +14,9 @@ export function projectStock(result) {
14
14
  const a = result.availabilities?.[0];
15
15
  return {
16
16
  availableForCashCarry: a?.availableForCashCarry ?? false,
17
- quantity: a?.buyingOption.cashCarry.availability.quantity ?? null,
18
- messageType: a?.buyingOption.cashCarry.availability.probability.thisDay.messageType ?? null,
19
- eligibleForStockNotification: a?.buyingOption.cashCarry.eligibleForStockNotification ?? null,
17
+ quantity: a?.buyingOption?.cashCarry?.availability?.quantity ?? null,
18
+ messageType: a?.buyingOption?.cashCarry?.availability?.probability?.thisDay?.messageType ?? null,
19
+ eligibleForStockNotification: a?.buyingOption?.cashCarry?.eligibleForStockNotification ?? null,
20
20
  errors: annotateStockErrors(result.errors) ?? [],
21
21
  };
22
22
  }
@@ -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
  }
@@ -48,8 +49,9 @@ export function projectMultiItemStock(result, itemNos) {
48
49
  return {
49
50
  itemNo,
50
51
  availableForCashCarry: a.availableForCashCarry,
51
- quantity: a.buyingOption.cashCarry.availability.quantity,
52
- messageType: a.buyingOption.cashCarry.availability.probability.thisDay.messageType,
52
+ quantity: a.buyingOption?.cashCarry?.availability?.quantity ?? null,
53
+ messageType: a.buyingOption?.cashCarry?.availability?.probability?.thisDay?.messageType ?? null,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikea-mcp",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "Read-only MCP server for IKEA product search and in-store stock lookup. Supports stdio and Streamable HTTP transports.",
5
5
  "license": "MIT",
6
6
  "type": "module",