ikea-mcp 1.1.0 → 1.2.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
@@ -17,9 +17,9 @@ Read-only MCP server for IKEA product search and in-store stock lookup.
17
17
 
18
18
  ## MVP limitations
19
19
  - Uses unofficial public IKEA APIs — no SLA, may break without notice
20
- - ~50 confirmed US stores in catalog (`src/data/stores.ts`); some IDs remain unverified
20
+ - ~65 confirmed US and Canada stores in catalog (`src/data/stores.ts`); some IDs remain unverified
21
21
  - Cash-and-carry availability only — click-and-collect and home delivery not exposed
22
- - HTTP transport has no auth — do not expose publicly without adding authentication
22
+ - HTTP transport is open by default — set `API_KEY` env var to require `x-api-key` header on `/mcp`
23
23
  - Read-only — no cart, order, or account operations
24
24
 
25
25
  ## Tools
@@ -190,7 +190,51 @@ Find stores with the highest in-stock quantity for an item. Queries stores in pa
190
190
  ]
191
191
  ```
192
192
 
193
- Returns `[]` if no store has the item in stock. "All known stores" means the ~50 entries in `src/data/stores.ts`.
193
+ Returns `[]` if no store has the item in stock. "All known stores" means the ~65 US and Canada entries in `src/data/stores.ts`.
194
+
195
+ ---
196
+
197
+ ## Example workflows
198
+
199
+ ### 1. Search → inspect → check one store
200
+
201
+ ```
202
+ 1. search_products { "query": "BILLY bookcase" }
203
+ → pick itemNo from results, e.g. "20522046"
204
+
205
+ 2. get_product_details { "itemNo": "20522046" }
206
+ → confirms name, price, dimensions before checking stock
207
+
208
+ 3. check_store_stock { "itemNo": "20522046", "storeId": "399" }
209
+ → { "availableForCashCarry": true, "quantity": 95, "messageType": "HIGH_IN_STOCK" }
210
+ ```
211
+
212
+ ### 2. Shopping list at one store
213
+
214
+ Check whether several items are available in a single trip:
215
+
216
+ ```json
217
+ {
218
+ "tool": "check_multi_item_stock",
219
+ "storeId": "399",
220
+ "itemNos": ["20522046", "40477340", "89268919"]
221
+ }
222
+ ```
223
+
224
+ Returns one entry per item in the same order — items not stocked appear with `availableForCashCarry: false` and a 404 error.
225
+
226
+ ### 3. Best store from a mixed US + Canada subset
227
+
228
+ ```json
229
+ {
230
+ "tool": "find_best_store_for_item",
231
+ "itemNo": "20522046",
232
+ "storeIds": ["399", "039", "216", "149", "026"],
233
+ "maxResults": 3
234
+ }
235
+ ```
236
+
237
+ Returns the top 3 stores by in-stock quantity across the mixed US/Canada subset. Omit `storeIds` to search all ~65 known stores.
194
238
 
195
239
  ---
196
240
 
@@ -244,7 +288,11 @@ Endpoints after deploy:
244
288
  - `POST /mcp` — MCP Streamable HTTP (requires `Accept: application/json, text/event-stream`)
245
289
  - `GET /health` — returns `{"status":"ok"}`
246
290
 
247
- > **Security note:** No auth is implemented in this MVP. Do not expose the `/mcp` endpoint publicly without adding authentication. The server is read-only — no cart, order, or account operations are possible.
291
+ > **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.
292
+ >
293
+ > ```bash
294
+ > API_KEY=your-secret node dist/http.js
295
+ > ```
248
296
 
249
297
  ## Connecting a local MCP client (stdio)
250
298
 
@@ -302,12 +350,12 @@ The `Accept: application/json, text/event-stream` header is required by the MCP
302
350
 
303
351
  ## Store IDs
304
352
 
305
- Store metadata (ID → city label) lives in `src/data/stores.ts`. ~50 US stores are confirmed from `ikea.com/us/en/stores/<slug>/` pages.
353
+ 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).
306
354
 
307
355
  Confirmed compatible `storeId` formats:
308
- - Standard 3-digit: `"399"` (Burbank, CA)
309
- - Leading-zero 3-digit: `"026"` (Canton, MI), `"042"` (Tampa, FL)
310
- - 4-digit: `"921"` (Brooklyn, NY), `"1129"` (Syracuse, NY)
356
+ - Standard 3-digit: `"399"` (Burbank, CA, US), `"216"` (Calgary, AB, CA)
357
+ - Leading-zero 3-digit: `"026"` (Canton, MI, US), `"039"` (Montreal, QC, CA)
358
+ - 4-digit: `"921"` (Brooklyn, NY, US), `"1129"` (Syracuse, NY, US)
311
359
 
312
360
  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`.
313
361
 
@@ -1,10 +1,14 @@
1
1
  // US store labels confirmed from ikea.com/us/en/stores/<slug>/ pages (store_id= survey param).
2
+ // CA store labels confirmed from ikea.com/ca/en/stores/<slug>/ pages (storeNo= embedded JSON).
2
3
  // IDs match the sto/{storeId} path used by the IKEA stock API.
3
4
  // 4-digit IDs (921, 1099, 1129) are newer-format stores — confirmed working with stock API.
4
5
  // 3136 (San Francisco) returns 405 from stock API — excluded.
5
- // TODO: huntsville, arcadia, mcallen-pharr, san-marcos — IDs not found on store pages.
6
- // TODO: 313, 372, 414, 448, 529, 559, 612, 634, 652, 661, 663, 692, 693 — valid API IDs, city unknown (possibly CA/other).
6
+ // 698 (CA — unknown city) returns 422 from stock API — excluded.
7
+ // 659 (Toronto Downtown) returns 200 but null quantity — included conservatively.
8
+ // TODO: huntsville, arcadia, mcallen-pharr, san-marcos (US) — IDs not found on store pages.
9
+ // TODO: 448, 612, 634, 652, 661, 663, 692, 693 — valid API IDs, city/country unknown.
7
10
  export const STORE_LABELS = {
11
+ // ── United States ────────────────────────────────────────────────────────────
8
12
  "026": "026 (Canton, MI)",
9
13
  "027": "027 (Round Rock, TX)",
10
14
  "028": "028 (Portland, OR)",
@@ -57,6 +61,24 @@ export const STORE_LABELS = {
57
61
  "921": "921 (Brooklyn, NY)",
58
62
  "1099": "1099 (University Park, TX)",
59
63
  "1129": "1129 (Syracuse, NY)",
64
+ // ── Canada ───────────────────────────────────────────────────────────────────
65
+ // Confirmed from ikea.com/ca/en/stores/<slug>/ pages (storeNo= embedded JSON).
66
+ // All probed against stock API with item 20522046 — all return 200.
67
+ "003": "003 (Richmond, BC, CA)",
68
+ "004": "004 (Ottawa, ON, CA)",
69
+ "039": "039 (Montreal, QC, CA)",
70
+ "040": "040 (Burlington, ON, CA)",
71
+ "149": "149 (North York, ON, CA)",
72
+ "216": "216 (Calgary, AB, CA)",
73
+ "249": "249 (Winnipeg, MB, CA)",
74
+ "256": "256 (Etobicoke, ON, CA)",
75
+ "313": "313 (Coquitlam, BC, CA)",
76
+ "349": "349 (Edmonton, AB, CA)",
77
+ "372": "372 (Vaughan, ON, CA)",
78
+ "414": "414 (Boucherville, QC, CA)",
79
+ "529": "529 (Halifax, NS, CA)",
80
+ "559": "559 (Quebec City, QC, CA)",
81
+ "659": "659 (Toronto Downtown, ON, CA)",
60
82
  };
61
83
  export function storeLabel(storeId) {
62
84
  return STORE_LABELS[storeId];
package/dist/http.js CHANGED
@@ -3,6 +3,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
3
3
  import { createServer } from "./server.js";
4
4
  const PORT = Number(process.env.PORT ?? 3000);
5
5
  const BODY_LIMIT = 1024 * 1024; // 1 MB
6
+ const API_KEY = process.env.API_KEY ?? "";
6
7
  function readBody(req) {
7
8
  return new Promise((resolve, reject) => {
8
9
  let raw = "";
@@ -40,6 +41,10 @@ const httpServer = createHttpServer(async (req, res) => {
40
41
  sendJson(res, 200, { status: "ok" });
41
42
  return;
42
43
  }
44
+ if (API_KEY && req.headers["x-api-key"] !== API_KEY) {
45
+ sendJson(res, 401, { error: "unauthorized" });
46
+ return;
47
+ }
43
48
  if (method === "POST" && url === "/mcp") {
44
49
  const body = await readBody(req);
45
50
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
@@ -60,4 +65,5 @@ const httpServer = createHttpServer(async (req, res) => {
60
65
  httpServer.listen(PORT, () => {
61
66
  console.error(`ikea-mcp HTTP listening on http://localhost:${PORT}/mcp`);
62
67
  console.error(`health: http://localhost:${PORT}/health`);
68
+ console.error(`auth: ${API_KEY ? "API_KEY required (x-api-key header)" : "open (set API_KEY to enable)"}`);
63
69
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikea-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",