daiso 1.0.5 → 1.0.7

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.
Files changed (64) hide show
  1. package/README.md +101 -7
  2. package/dist/api/actionsProxy.js +111 -21
  3. package/dist/api/configStatus.js +23 -0
  4. package/dist/api/gs25Handlers.js +68 -22
  5. package/dist/api/handlers.js +32 -3
  6. package/dist/api/healthChecks.js +233 -20
  7. package/dist/api/lottemartHandlers.js +17 -1
  8. package/dist/api/placesHandlers.js +44 -0
  9. package/dist/api/response.js +2 -0
  10. package/dist/api/routes/healthRoutes.js +20 -3
  11. package/dist/api/routes/placesRoutes.js +9 -0
  12. package/dist/bin.js +0 -0
  13. package/dist/cli/args.js +21 -0
  14. package/dist/cli/commandOptions.js +96 -0
  15. package/dist/cli/commands/convenience.js +73 -1
  16. package/dist/cli/commands/daiso.js +18 -2
  17. package/dist/cli/http.js +58 -0
  18. package/dist/cli/interactiveItemSearch.js +4 -2
  19. package/dist/cli/interactivePrompt.js +3 -2
  20. package/dist/cli.js +25 -1
  21. package/dist/cliHelp.js +20 -6
  22. package/dist/cliInteractive.js +75 -6
  23. package/dist/cliPicker.js +1 -0
  24. package/dist/cliRenderer.js +93 -3
  25. package/dist/core/errors.js +66 -0
  26. package/dist/core/registry.js +246 -3
  27. package/dist/index.js +19 -4
  28. package/dist/pages/openapiFullSpec.js +3 -0
  29. package/dist/pages/openapiSpecActions.js +308 -42
  30. package/dist/pages/openapiSpecPathsCu.js +7 -0
  31. package/dist/pages/openapiSpecPathsLotteMart.js +14 -0
  32. package/dist/pages/openapiSpecPathsOliveyoung.js +35 -0
  33. package/dist/pages/openapiSpecPathsPlaces.js +85 -0
  34. package/dist/pages/promptReferenceText.js +16 -7
  35. package/dist/pages/promptRetailCinemaText.js +24 -2
  36. package/dist/services/daiso/client.js +8 -0
  37. package/dist/services/daiso/index.js +2 -0
  38. package/dist/services/daiso/tools/checkInventory.js +4 -2
  39. package/dist/services/daiso/tools/findInventoryByName.js +222 -0
  40. package/dist/services/daiso/tools/getDisplayLocation.js +2 -2
  41. package/dist/services/emart24/client.js +9 -0
  42. package/dist/services/emart24/tools/searchProducts.js +27 -17
  43. package/dist/services/gs25/api.js +3 -0
  44. package/dist/services/gs25/client.js +216 -30
  45. package/dist/services/gs25/index.js +11 -3
  46. package/dist/services/gs25/tools/checkInventory.js +39 -12
  47. package/dist/services/gs25/tools/findNearbyStores.js +19 -5
  48. package/dist/services/gs25/tools/searchProducts.js +39 -25
  49. package/dist/services/lottemart/client.js +10 -1
  50. package/dist/services/lottemart/tools/searchProducts.js +8 -1
  51. package/dist/services/lottemart/zetta.js +1 -1
  52. package/dist/services/oliveyoung/client.js +206 -86
  53. package/dist/services/oliveyoung/tools/checkInventory.js +7 -2
  54. package/dist/services/places/client.js +116 -0
  55. package/dist/services/places/index.js +23 -0
  56. package/dist/services/places/tools/searchNearby.js +56 -0
  57. package/dist/services/places/types.js +1 -0
  58. package/dist/services/seveneleven/client.js +7 -0
  59. package/dist/services/seveneleven/tools/searchProducts.js +29 -18
  60. package/dist/utils/http.js +82 -10
  61. package/dist/utils/zyte.js +74 -26
  62. package/openapi.json +22 -3
  63. package/openapi.yaml +16 -3
  64. package/package.json +28 -9
@@ -3,6 +3,12 @@
3
3
  */
4
4
  import * as z from 'zod';
5
5
  import { searchSevenElevenProductsWithVariants } from '../productKeyword.js';
6
+ function buildTextResponse(payload) {
7
+ return {
8
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
9
+ structuredContent: payload,
10
+ };
11
+ }
6
12
  async function searchProducts(args) {
7
13
  const { query, page = 1, size = 20, sort = 'recommend', timeoutMs = 15000 } = args;
8
14
  if (!query || query.trim().length === 0) {
@@ -14,25 +20,29 @@ async function searchProducts(args) {
14
20
  sort,
15
21
  timeout: timeoutMs,
16
22
  });
17
- return {
18
- content: [
19
- {
20
- type: 'text',
21
- text: JSON.stringify({
22
- query: result.query,
23
- page,
24
- size,
25
- sort,
26
- totalCount: result.totalCount,
27
- count: result.products.length,
28
- collectionIds: result.collectionIds,
29
- appliedQueries: result.appliedQueries,
30
- products: result.products,
31
- }, null, 2),
32
- },
33
- ],
34
- };
23
+ return buildTextResponse({
24
+ query: result.query,
25
+ page,
26
+ size,
27
+ sort,
28
+ totalCount: result.totalCount,
29
+ count: result.products.length,
30
+ collectionIds: result.collectionIds,
31
+ appliedQueries: result.appliedQueries,
32
+ products: result.products,
33
+ });
35
34
  }
35
+ const searchProductsOutputSchema = {
36
+ query: z.string().describe('적용된 검색어'),
37
+ page: z.number().describe('페이지 번호'),
38
+ size: z.number().describe('페이지 크기'),
39
+ sort: z.string().describe('정렬 기준'),
40
+ totalCount: z.number().describe('검색된 전체 상품 수'),
41
+ count: z.number().describe('반환된 상품 수'),
42
+ collectionIds: z.array(z.string()).describe('검색 컬렉션 ID 목록'),
43
+ appliedQueries: z.array(z.string()).describe('실제로 시도한 검색어 목록'),
44
+ products: z.array(z.unknown()).describe('세븐일레븐 상품 검색 결과'),
45
+ };
36
46
  export function createSearchProductsTool() {
37
47
  return {
38
48
  name: 'seveneleven_search_products',
@@ -50,6 +60,7 @@ export function createSearchProductsTool() {
50
60
  .describe('정렬 기준 (기본값: recommend)'),
51
61
  timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
52
62
  },
63
+ outputSchema: searchProductsOutputSchema,
53
64
  },
54
65
  handler: searchProducts,
55
66
  };
@@ -20,18 +20,90 @@ export function createTimeoutController(timeout) {
20
20
  const timeoutId = setTimeout(() => controller.abort(), timeout);
21
21
  return { controller, timeoutId };
22
22
  }
23
- export async function fetchWithTimeout(url, options = {}) {
24
- const { timeout = 10000, ...restOptions } = options;
25
- const { controller, timeoutId } = createTimeoutController(timeout);
26
- try {
27
- return await fetch(url, {
28
- ...restOptions,
29
- signal: controller.signal,
30
- });
23
+ const DEFAULT_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504, 522, 524];
24
+ const DEFAULT_RETRY_METHODS = ['GET', 'HEAD', 'OPTIONS'];
25
+ function isRetryableStatus(status, retryStatusCodes) {
26
+ return retryStatusCodes.includes(status);
27
+ }
28
+ function wait(ms) {
29
+ if (ms <= 0) {
30
+ return Promise.resolve();
31
+ }
32
+ return new Promise((resolve) => {
33
+ setTimeout(resolve, ms);
34
+ });
35
+ }
36
+ function normalizeMethod(method) {
37
+ return (method || 'GET').toUpperCase();
38
+ }
39
+ function canRetryMethod(method, retryMethods, retryUnsafeMethods) {
40
+ return retryUnsafeMethods || retryMethods.map((item) => item.toUpperCase()).includes(method);
41
+ }
42
+ function parseRetryAfterDelayMs(response) {
43
+ const retryAfter = response.headers.get('retry-after');
44
+ if (!retryAfter) {
45
+ return null;
46
+ }
47
+ const seconds = Number.parseFloat(retryAfter);
48
+ if (Number.isFinite(seconds)) {
49
+ return Math.max(0, Math.trunc(seconds * 1000));
50
+ }
51
+ const retryDate = Date.parse(retryAfter);
52
+ if (Number.isFinite(retryDate)) {
53
+ return Math.max(0, retryDate - Date.now());
31
54
  }
32
- finally {
33
- clearTimeout(timeoutId);
55
+ return null;
56
+ }
57
+ export async function fetchWithTimeout(url, options = {}) {
58
+ const { timeout = 10000, retries = 0, retryDelayMs = 250, retryStatusCodes = DEFAULT_RETRY_STATUS_CODES, retryUnsafeMethods = false, retryMethods = DEFAULT_RETRY_METHODS, onRetry, ...restOptions } = options;
59
+ const maxAttempts = Math.max(1, Math.trunc(retries) + 1);
60
+ const method = normalizeMethod(restOptions.method);
61
+ const retryAllowed = canRetryMethod(method, retryMethods, retryUnsafeMethods);
62
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
63
+ const { controller, timeoutId } = createTimeoutController(timeout);
64
+ try {
65
+ const response = await fetch(url, {
66
+ ...restOptions,
67
+ signal: controller.signal,
68
+ });
69
+ if (retryAllowed && attempt < maxAttempts && isRetryableStatus(response.status, retryStatusCodes)) {
70
+ const delayMs = parseRetryAfterDelayMs(response) ?? retryDelayMs;
71
+ onRetry?.({
72
+ attempt,
73
+ maxAttempts,
74
+ delayMs,
75
+ reason: 'status',
76
+ status: response.status,
77
+ method,
78
+ url,
79
+ });
80
+ await response.body?.cancel();
81
+ await wait(delayMs);
82
+ continue;
83
+ }
84
+ return response;
85
+ }
86
+ catch (error) {
87
+ if (!retryAllowed || attempt >= maxAttempts) {
88
+ throw error;
89
+ }
90
+ onRetry?.({
91
+ attempt,
92
+ maxAttempts,
93
+ delayMs: retryDelayMs,
94
+ reason: 'error',
95
+ error,
96
+ method,
97
+ url,
98
+ });
99
+ await wait(retryDelayMs);
100
+ }
101
+ finally {
102
+ clearTimeout(timeoutId);
103
+ }
34
104
  }
105
+ /* v8 ignore next */
106
+ throw new Error('API 요청 재시도 처리 중 알 수 없는 오류가 발생했습니다.');
35
107
  }
36
108
  export async function fetchJson(url, options = {}) {
37
109
  const response = await fetchWithTimeout(url, options);
@@ -37,36 +37,84 @@ export function decodeBase64(value) {
37
37
  /* c8 ignore end */
38
38
  throw new Error('Base64 디코딩을 지원하지 않는 런타임입니다.');
39
39
  }
40
+ function isAbortError(error) {
41
+ return error instanceof Error && error.name === 'AbortError';
42
+ }
43
+ class RetryableZyteError extends Error {
44
+ }
45
+ function isRetryableZyteStatus(status) {
46
+ return status === 408 || status === 429 || status >= 500;
47
+ }
48
+ function isRetryableTargetStatus(statusCode) {
49
+ return typeof statusCode === 'number' && statusCode >= 500;
50
+ }
51
+ function isRetryableError(error) {
52
+ return isAbortError(error) || error instanceof RetryableZyteError;
53
+ }
54
+ function wait(ms) {
55
+ if (ms <= 0) {
56
+ return Promise.resolve();
57
+ }
58
+ return new Promise((resolve) => {
59
+ setTimeout(resolve, ms);
60
+ });
61
+ }
40
62
  export async function requestByZyte(options) {
41
- const { timeout = 15000, url, method = 'GET', headers = [], bodyText, apiKey } = options;
63
+ const { timeout = 15000, retries = 1, retryDelayMs = 250, url, method = 'GET', headers = [], bodyText, apiKey, } = options;
42
64
  const auth = encodeBasicAuth(resolveZyteApiKey(apiKey));
43
- const { controller, timeoutId } = createTimeoutController(timeout);
44
- try {
45
- const response = await fetch('https://api.zyte.com/v1/extract', {
46
- method: 'POST',
47
- headers: {
48
- Authorization: `Basic ${auth}`,
49
- 'Content-Type': 'application/json',
50
- Accept: 'application/json',
51
- },
52
- body: JSON.stringify({
53
- url,
54
- httpRequestMethod: method,
55
- customHttpRequestHeaders: headers,
56
- httpRequestText: bodyText,
57
- httpResponseBody: true,
58
- }),
59
- signal: controller.signal,
60
- });
61
- const result = (await response.json());
62
- if (!response.ok) {
63
- throw new Error(`Zyte API 호출 실패: ${response.status} ${result.detail || result.title || ''}`.trim());
65
+ const maxAttempts = Math.max(1, Math.trunc(retries) + 1);
66
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
67
+ const { controller, timeoutId } = createTimeoutController(timeout);
68
+ try {
69
+ let response;
70
+ try {
71
+ response = await fetch('https://api.zyte.com/v1/extract', {
72
+ method: 'POST',
73
+ headers: {
74
+ Authorization: `Basic ${auth}`,
75
+ 'Content-Type': 'application/json',
76
+ Accept: 'application/json',
77
+ },
78
+ body: JSON.stringify({
79
+ url,
80
+ httpRequestMethod: method,
81
+ customHttpRequestHeaders: headers,
82
+ httpRequestText: bodyText,
83
+ httpResponseBody: true,
84
+ }),
85
+ signal: controller.signal,
86
+ });
87
+ }
88
+ catch (error) {
89
+ if (error instanceof TypeError) {
90
+ throw new RetryableZyteError(error.message);
91
+ }
92
+ throw error;
93
+ }
94
+ const result = (await response.json());
95
+ if (!response.ok) {
96
+ const message = `Zyte API 호출 실패: ${response.status} ${result.detail || result.title || ''}`.trim();
97
+ if (isRetryableZyteStatus(response.status)) {
98
+ throw new RetryableZyteError(message);
99
+ }
100
+ throw new Error(message);
101
+ }
102
+ if (isRetryableTargetStatus(result.statusCode) && attempt < maxAttempts) {
103
+ throw new RetryableZyteError(`Zyte target response failed: ${result.statusCode}`);
104
+ }
105
+ return result;
106
+ }
107
+ catch (error) {
108
+ if (!isRetryableError(error) || attempt >= maxAttempts) {
109
+ throw error;
110
+ }
111
+ await wait(retryDelayMs);
112
+ }
113
+ finally {
114
+ clearTimeout(timeoutId);
64
115
  }
65
- return result;
66
- }
67
- finally {
68
- clearTimeout(timeoutId);
69
116
  }
117
+ throw new Error('Zyte API 호출 실패');
70
118
  }
71
119
  export function decodeZyteHttpBody(result) {
72
120
  if (!result.httpResponseBody) {
package/openapi.json CHANGED
@@ -53,6 +53,7 @@
53
53
  "sevenelevenCheckInventory",
54
54
  "sevenelevenGetPopwords",
55
55
  "sevenelevenGetCatalog",
56
+ "placesSearchNearby",
56
57
  "megaboxFindTheaters",
57
58
  "megaboxListMovies",
58
59
  "megaboxGetSeats",
@@ -96,7 +97,7 @@
96
97
  "name": "productId",
97
98
  "in": "query",
98
99
  "required": false,
99
- "description": "제품 ID 또는 상세 조회용 path 파라미터",
100
+ "description": "제품 ID 또는 상세 조회용 path 파라미터. 상품명만 알면 먼저 daisoSearchProducts 또는 /api/daiso/products로 productId를 확인하세요.",
100
101
  "schema": {
101
102
  "type": "string"
102
103
  }
@@ -110,6 +111,24 @@
110
111
  "type": "string"
111
112
  }
112
113
  },
114
+ {
115
+ "name": "location",
116
+ "in": "query",
117
+ "required": false,
118
+ "description": "주변 장소 검색 지역/역/주소 키워드",
119
+ "schema": {
120
+ "type": "string"
121
+ }
122
+ },
123
+ {
124
+ "name": "category",
125
+ "in": "query",
126
+ "required": false,
127
+ "description": "주변 장소 카테고리(restaurant, cafe, food, dessert, all)",
128
+ "schema": {
129
+ "type": "string"
130
+ }
131
+ },
113
132
  {
114
133
  "name": "sido",
115
134
  "in": "query",
@@ -278,7 +297,7 @@
278
297
  "name": "storeCode",
279
298
  "in": "query",
280
299
  "required": false,
281
- "description": "매장 코드",
300
+ "description": "매장 코드. 다이소 진열 위치는 /api/daiso/inventory 응답의 storeInventory.stores[].storeCode에서 확인하세요.",
282
301
  "schema": {
283
302
  "type": "string"
284
303
  }
@@ -287,7 +306,7 @@
287
306
  "name": "storeName",
288
307
  "in": "query",
289
308
  "required": false,
290
- "description": "매장명",
309
+ "description": "매장명. 롯데마트 상품 조회에서 storeCode가 없을 때 사용하며, 모르면 먼저 lottemartFindNearbyStores 또는 /api/lottemart/stores로 확인하세요.",
291
310
  "schema": {
292
311
  "type": "string"
293
312
  }
package/openapi.yaml CHANGED
@@ -54,6 +54,7 @@ paths:
54
54
  - sevenelevenCheckInventory
55
55
  - sevenelevenGetPopwords
56
56
  - sevenelevenGetCatalog
57
+ - placesSearchNearby
57
58
  - megaboxFindTheaters
58
59
  - megaboxListMovies
59
60
  - megaboxGetSeats
@@ -84,7 +85,7 @@ paths:
84
85
  - name: productId
85
86
  in: query
86
87
  required: false
87
- description: 제품 ID 또는 상세 조회용 path 파라미터
88
+ description: 제품 ID 또는 상세 조회용 path 파라미터. 상품명만 알면 먼저 daisoSearchProducts 또는 /api/daiso/products로 productId를 확인하세요.
88
89
  schema:
89
90
  type: string
90
91
  - name: keyword
@@ -93,6 +94,18 @@ paths:
93
94
  description: 공통 키워드 파라미터
94
95
  schema:
95
96
  type: string
97
+ - name: location
98
+ in: query
99
+ required: false
100
+ description: 주변 장소 검색 지역/역/주소 키워드
101
+ schema:
102
+ type: string
103
+ - name: category
104
+ in: query
105
+ required: false
106
+ description: 주변 장소 카테고리(restaurant, cafe, food, dessert, all)
107
+ schema:
108
+ type: string
96
109
  - name: sido
97
110
  in: query
98
111
  required: false
@@ -206,13 +219,13 @@ paths:
206
219
  - name: storeCode
207
220
  in: query
208
221
  required: false
209
- description: 매장 코드
222
+ description: 매장 코드. 다이소 진열 위치는 /api/daiso/inventory 응답의 storeInventory.stores[].storeCode에서 확인하세요.
210
223
  schema:
211
224
  type: string
212
225
  - name: storeName
213
226
  in: query
214
227
  required: false
215
- description: 매장명
228
+ description: 매장명. 롯데마트 상품 조회에서 storeCode가 없을 때 사용하며, 모르면 먼저 lottemartFindNearbyStores 또는 /api/lottemart/stores로 확인하세요.
216
229
  schema:
217
230
  type: string
218
231
  - name: storeKeyword
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "daiso",
3
- "version": "1.0.5",
4
- "description": "Daiso MCP Server for mobile access via Cloudflare Workers",
3
+ "version": "1.0.7",
4
+ "description": "Korean retail and cinema MCP server, CLI, and Codex Skill for Daiso, convenience stores, marts, Olive Young, and theaters",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "daiso": "dist/bin.js"
@@ -21,10 +21,11 @@
21
21
  "hooks:install": "git config core.hooksPath .githooks && chmod +x .githooks/pre-push",
22
22
  "dev": "wrangler dev",
23
23
  "deploy": "wrangler deploy",
24
- "build": "tsc && npx tsx scripts/generate-openapi.ts",
25
- "build:openapi": "npx tsx scripts/generate-openapi.ts",
26
- "cli:smoke": "npm run build && npx tsx scripts/cli-smoke.ts",
27
- "update:workers-chart": "npx tsx scripts/update-workers-invocations-chart.ts",
24
+ "build": "tsc && npx tsx scripts/ops/generate-openapi.ts",
25
+ "build:openapi": "npx tsx scripts/ops/generate-openapi.ts",
26
+ "cli:smoke": "npm run build && npx tsx scripts/ops/cli-smoke.ts",
27
+ "mcp:smoke": "npx tsx scripts/ops/mcp-smoke.ts",
28
+ "update:workers-chart": "npx tsx scripts/ops/update-workers-invocations-chart.ts",
28
29
  "prepack": "npm run build",
29
30
  "prepublishOnly": "npm run check && npm run build",
30
31
  "release:dry-run": "npm pack --dry-run",
@@ -42,7 +43,24 @@
42
43
  },
43
44
  "keywords": [
44
45
  "mcp",
46
+ "mcp-server",
45
47
  "daiso",
48
+ "korea",
49
+ "korean-retail",
50
+ "retail",
51
+ "inventory",
52
+ "cli",
53
+ "codex-skill",
54
+ "clawhub",
55
+ "oliveyoung",
56
+ "gs25",
57
+ "cu",
58
+ "seveneleven",
59
+ "emart24",
60
+ "lottemart",
61
+ "cgv",
62
+ "megabox",
63
+ "lottecinema",
46
64
  "cloudflare-workers",
47
65
  "serverless"
48
66
  ],
@@ -50,14 +68,14 @@
50
68
  "license": "MIT",
51
69
  "repository": {
52
70
  "type": "git",
53
- "url": "https://github.com/hmmhmmhm/daiso-mcp.git"
71
+ "url": "git+https://github.com/hmmhmmhm/daiso-mcp.git"
54
72
  },
55
73
  "homepage": "https://github.com/hmmhmmhm/daiso-mcp#readme",
56
74
  "bugs": {
57
75
  "url": "https://github.com/hmmhmmhm/daiso-mcp/issues"
58
76
  },
59
77
  "engines": {
60
- "node": ">=20 <21"
78
+ "node": ">=20"
61
79
  },
62
80
  "devDependencies": {
63
81
  "@cloudflare/workers-types": "^4.20260316.1",
@@ -82,7 +100,8 @@
82
100
  "overrides": {
83
101
  "flatted": "3.4.2",
84
102
  "undici": "7.24.0",
85
- "brace-expansion": "5.0.6"
103
+ "brace-expansion": "5.0.6",
104
+ "ws": "8.20.1"
86
105
  },
87
106
  "dependencies": {
88
107
  "@modelcontextprotocol/sdk": "^1.27.1",