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.
- package/README.md +101 -7
- package/dist/api/actionsProxy.js +111 -21
- package/dist/api/configStatus.js +23 -0
- package/dist/api/gs25Handlers.js +68 -22
- package/dist/api/handlers.js +32 -3
- package/dist/api/healthChecks.js +233 -20
- package/dist/api/lottemartHandlers.js +17 -1
- package/dist/api/placesHandlers.js +44 -0
- package/dist/api/response.js +2 -0
- package/dist/api/routes/healthRoutes.js +20 -3
- package/dist/api/routes/placesRoutes.js +9 -0
- package/dist/bin.js +0 -0
- package/dist/cli/args.js +21 -0
- package/dist/cli/commandOptions.js +96 -0
- package/dist/cli/commands/convenience.js +73 -1
- package/dist/cli/commands/daiso.js +18 -2
- package/dist/cli/http.js +58 -0
- package/dist/cli/interactiveItemSearch.js +4 -2
- package/dist/cli/interactivePrompt.js +3 -2
- package/dist/cli.js +25 -1
- package/dist/cliHelp.js +20 -6
- package/dist/cliInteractive.js +75 -6
- package/dist/cliPicker.js +1 -0
- package/dist/cliRenderer.js +93 -3
- package/dist/core/errors.js +66 -0
- package/dist/core/registry.js +246 -3
- package/dist/index.js +19 -4
- package/dist/pages/openapiFullSpec.js +3 -0
- package/dist/pages/openapiSpecActions.js +308 -42
- package/dist/pages/openapiSpecPathsCu.js +7 -0
- package/dist/pages/openapiSpecPathsLotteMart.js +14 -0
- package/dist/pages/openapiSpecPathsOliveyoung.js +35 -0
- package/dist/pages/openapiSpecPathsPlaces.js +85 -0
- package/dist/pages/promptReferenceText.js +16 -7
- package/dist/pages/promptRetailCinemaText.js +24 -2
- package/dist/services/daiso/client.js +8 -0
- package/dist/services/daiso/index.js +2 -0
- package/dist/services/daiso/tools/checkInventory.js +4 -2
- package/dist/services/daiso/tools/findInventoryByName.js +222 -0
- package/dist/services/daiso/tools/getDisplayLocation.js +2 -2
- package/dist/services/emart24/client.js +9 -0
- package/dist/services/emart24/tools/searchProducts.js +27 -17
- package/dist/services/gs25/api.js +3 -0
- package/dist/services/gs25/client.js +216 -30
- package/dist/services/gs25/index.js +11 -3
- package/dist/services/gs25/tools/checkInventory.js +39 -12
- package/dist/services/gs25/tools/findNearbyStores.js +19 -5
- package/dist/services/gs25/tools/searchProducts.js +39 -25
- package/dist/services/lottemart/client.js +10 -1
- package/dist/services/lottemart/tools/searchProducts.js +8 -1
- package/dist/services/lottemart/zetta.js +1 -1
- package/dist/services/oliveyoung/client.js +206 -86
- package/dist/services/oliveyoung/tools/checkInventory.js +7 -2
- package/dist/services/places/client.js +116 -0
- package/dist/services/places/index.js +23 -0
- package/dist/services/places/tools/searchNearby.js +56 -0
- package/dist/services/places/types.js +1 -0
- package/dist/services/seveneleven/client.js +7 -0
- package/dist/services/seveneleven/tools/searchProducts.js +29 -18
- package/dist/utils/http.js +82 -10
- package/dist/utils/zyte.js +74 -26
- package/openapi.json +22 -3
- package/openapi.yaml +16 -3
- package/package.json +28 -9
package/dist/cliInteractive.js
CHANGED
|
@@ -10,6 +10,42 @@ import { fetchEnvelope, fetchStoresWithKeywordFallback } from './cli/interactive
|
|
|
10
10
|
import { askMenu, askNextAction, askNonEmpty, askYesNo, printStoreDetail } from './cli/interactivePrompt.js';
|
|
11
11
|
import { parseLotteCinemaTheaters, printTheaterDetail, runCuItemSearch, runDaisoItemSearch, runLotteCinemaSearch, runOliveyoungItemSearch, } from './cli/interactiveItemSearch.js';
|
|
12
12
|
import { buildDaisoStoreKeywordVariants, isRecord, parseDaisoProducts, parseStores, toText } from './utils/cliInteractiveHelpers.js';
|
|
13
|
+
function isLocationToken(token) {
|
|
14
|
+
return /(?:역|점|동|구|시|군|읍|면|로|길)$/.test(token);
|
|
15
|
+
}
|
|
16
|
+
export function inferDaisoFreeTextIntent(input) {
|
|
17
|
+
const tokens = input.trim().split(/\s+/).filter(Boolean);
|
|
18
|
+
if (tokens.length <= 1) {
|
|
19
|
+
const value = tokens[0] || input.trim();
|
|
20
|
+
return { productQuery: value, storeKeyword: value };
|
|
21
|
+
}
|
|
22
|
+
const locationIndex = tokens.findIndex(isLocationToken);
|
|
23
|
+
if (locationIndex >= 0) {
|
|
24
|
+
const storeKeyword = tokens[locationIndex];
|
|
25
|
+
const productQuery = tokens.filter((_, index) => index !== locationIndex).join(' ').trim() || input.trim();
|
|
26
|
+
return { productQuery, storeKeyword };
|
|
27
|
+
}
|
|
28
|
+
const storeKeyword = tokens[tokens.length - 1];
|
|
29
|
+
const productQuery = tokens.slice(0, -1).join(' ').trim() || input.trim();
|
|
30
|
+
return { productQuery, storeKeyword };
|
|
31
|
+
}
|
|
32
|
+
async function askServiceChoice(prompt, writeOut) {
|
|
33
|
+
while (true) {
|
|
34
|
+
const answer = await prompt.ask('서비스 번호를 선택하세요 (0: 종료): ');
|
|
35
|
+
const picked = Number.parseInt(answer, 10);
|
|
36
|
+
if (!Number.isNaN(picked) && String(picked) === answer.trim()) {
|
|
37
|
+
if (picked >= 0 && picked <= 4) {
|
|
38
|
+
return picked;
|
|
39
|
+
}
|
|
40
|
+
writeOut('번호로 입력하세요. 0부터 4 사이에서 선택할 수 있습니다.');
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (answer.trim().length > 0) {
|
|
44
|
+
return answer.trim();
|
|
45
|
+
}
|
|
46
|
+
writeOut('번호로 입력하세요. 0부터 4 사이에서 선택할 수 있습니다.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
13
49
|
function createPrompt() {
|
|
14
50
|
const rl = createInterface({ input, output });
|
|
15
51
|
return {
|
|
@@ -38,6 +74,7 @@ export const cliInteractiveTestables = {
|
|
|
38
74
|
askNonEmpty,
|
|
39
75
|
askNextAction,
|
|
40
76
|
printStoreDetail,
|
|
77
|
+
inferDaisoFreeTextIntent,
|
|
41
78
|
runDaisoItemSearch,
|
|
42
79
|
runOliveyoungItemSearch,
|
|
43
80
|
runCuItemSearch,
|
|
@@ -55,12 +92,44 @@ export async function runInteractiveCli(deps) {
|
|
|
55
92
|
deps.writeOut('2. 올리브영');
|
|
56
93
|
deps.writeOut('3. CU');
|
|
57
94
|
deps.writeOut('4. 롯데시네마');
|
|
58
|
-
const serviceChoice = await
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
'
|
|
62
|
-
|
|
63
|
-
|
|
95
|
+
const serviceChoice = await askServiceChoice(prompt, deps.writeOut);
|
|
96
|
+
if (typeof serviceChoice === 'string') {
|
|
97
|
+
const intent = inferDaisoFreeTextIntent(serviceChoice);
|
|
98
|
+
deps.writeOut('자유 입력을 다이소 조회로 처리합니다.');
|
|
99
|
+
deps.writeOut(`- 상품 키워드: ${intent.productQuery}`);
|
|
100
|
+
deps.writeOut(`- 위치 키워드: ${intent.storeKeyword}`);
|
|
101
|
+
const storeResult = await fetchStoresWithKeywordFallback(deps.fetchImpl, 'daiso', intent.storeKeyword);
|
|
102
|
+
const stores = storeResult.stores;
|
|
103
|
+
if (stores.length === 0) {
|
|
104
|
+
deps.writeOut('검색된 매장이 없습니다.');
|
|
105
|
+
deps.writeOut('힌트: "안산 중앙역" 대신 "안산중앙" 또는 "고잔"으로 검색해보세요.');
|
|
106
|
+
keepRunning = await askYesNo(prompt, '다시 시도할까요? (y/n): ');
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const selectedStore = stores.length === 1
|
|
110
|
+
? stores[0]
|
|
111
|
+
: await pickFromList({
|
|
112
|
+
prompt,
|
|
113
|
+
writeOut: deps.writeOut,
|
|
114
|
+
title: '[매장 선택]',
|
|
115
|
+
emptyText: '검색된 매장이 없습니다.',
|
|
116
|
+
cancelText: '매장 검색으로 돌아갑니다.',
|
|
117
|
+
items: stores,
|
|
118
|
+
renderItem: (store, index) => `${index + 1}. ${store.name} | ${store.address || '주소 정보 없음'}`,
|
|
119
|
+
filterText: (store) => `${store.name} ${store.address} ${store.phone}`,
|
|
120
|
+
indexText: '입력: 번호 선택 | /키워드 필터 | all 전체보기 | 0 다시 검색',
|
|
121
|
+
});
|
|
122
|
+
if (!selectedStore) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
printStoreDetail(deps.writeOut, selectedStore);
|
|
126
|
+
await runDaisoItemSearch(deps, prompt, selectedStore, intent.productQuery);
|
|
127
|
+
const nextAction = await askNextAction(prompt, deps.writeOut);
|
|
128
|
+
if (nextAction === 'exit') {
|
|
129
|
+
keepRunning = false;
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
64
133
|
if (serviceChoice === 0) {
|
|
65
134
|
break;
|
|
66
135
|
}
|
package/dist/cliPicker.js
CHANGED
|
@@ -49,6 +49,7 @@ export async function pickFromList(options) {
|
|
|
49
49
|
}
|
|
50
50
|
const picked = Number.parseInt(answer, 10);
|
|
51
51
|
if (Number.isNaN(picked) || picked < 1 || picked > visibleItems.length) {
|
|
52
|
+
writeOut(`번호로 입력하세요. 1부터 ${visibleItems.length} 사이에서 선택하거나 0으로 취소하세요.`);
|
|
52
53
|
continue;
|
|
53
54
|
}
|
|
54
55
|
return visibleItems[picked - 1];
|
package/dist/cliRenderer.js
CHANGED
|
@@ -29,7 +29,17 @@ function formatMeta(meta) {
|
|
|
29
29
|
return [`메타: ${pairs.join(', ')}`];
|
|
30
30
|
}
|
|
31
31
|
function detectName(item) {
|
|
32
|
-
const keys = [
|
|
32
|
+
const keys = [
|
|
33
|
+
'name',
|
|
34
|
+
'productName',
|
|
35
|
+
'goodsNm',
|
|
36
|
+
'goodsName',
|
|
37
|
+
'itemName',
|
|
38
|
+
'searchItemName',
|
|
39
|
+
'storeName',
|
|
40
|
+
'theaterName',
|
|
41
|
+
'movieName',
|
|
42
|
+
];
|
|
33
43
|
for (const key of keys) {
|
|
34
44
|
if (typeof item[key] === 'string' && item[key].trim().length > 0) {
|
|
35
45
|
return item[key];
|
|
@@ -49,8 +59,24 @@ function formatCollection(title, items) {
|
|
|
49
59
|
continue;
|
|
50
60
|
}
|
|
51
61
|
const name = detectName(entry);
|
|
52
|
-
const id = entry.id ??
|
|
53
|
-
|
|
62
|
+
const id = entry.id ??
|
|
63
|
+
entry.productId ??
|
|
64
|
+
entry.PD_NO ??
|
|
65
|
+
entry.itemCode ??
|
|
66
|
+
entry.pluCd ??
|
|
67
|
+
entry.goodsNumber ??
|
|
68
|
+
entry.productNo ??
|
|
69
|
+
entry.storeCode ??
|
|
70
|
+
entry.bizNo ??
|
|
71
|
+
entry.theaterId ??
|
|
72
|
+
entry.movieId ??
|
|
73
|
+
entry.link;
|
|
74
|
+
const price = entry.price ??
|
|
75
|
+
entry.prc ??
|
|
76
|
+
entry.PD_PRC ??
|
|
77
|
+
entry.viewPrice ??
|
|
78
|
+
entry.priceToPay ??
|
|
79
|
+
entry.searchItemSellPrice;
|
|
54
80
|
let detail = `- ${name}`;
|
|
55
81
|
if (id !== undefined) {
|
|
56
82
|
detail += ` [${toText(id)}]`;
|
|
@@ -58,6 +84,22 @@ function formatCollection(title, items) {
|
|
|
58
84
|
if (price !== undefined) {
|
|
59
85
|
detail += ` / ${toText(price)}원`;
|
|
60
86
|
}
|
|
87
|
+
const detailParts = [
|
|
88
|
+
entry.address ?? entry.storeAddress,
|
|
89
|
+
entry.distance !== undefined ? `${toText(entry.distance)}km` : undefined,
|
|
90
|
+
entry.distanceKm !== undefined ? `${toText(entry.distanceKm)}km` : undefined,
|
|
91
|
+
entry.distanceM !== undefined ? `${toText(entry.distanceM)}m` : undefined,
|
|
92
|
+
entry.quantity !== undefined ? `수량 ${toText(entry.quantity)}` : undefined,
|
|
93
|
+
entry.stock !== undefined ? `수량 ${toText(entry.stock)}` : undefined,
|
|
94
|
+
entry.realStockQuantity !== undefined ? `수량 ${toText(entry.realStockQuantity)}` : undefined,
|
|
95
|
+
entry.bizQty !== undefined ? `수량 ${toText(entry.bizQty)}` : undefined,
|
|
96
|
+
entry.remainQuantity !== undefined ? `수량 ${toText(entry.remainQuantity)}` : undefined,
|
|
97
|
+
entry.o2oRemainQuantity !== undefined ? `수량 ${toText(entry.o2oRemainQuantity)}` : undefined,
|
|
98
|
+
entry.stockLabel,
|
|
99
|
+
].filter((value) => value !== undefined);
|
|
100
|
+
if (detailParts.length > 0) {
|
|
101
|
+
detail += ` / ${detailParts.map(toText).join(' / ')}`;
|
|
102
|
+
}
|
|
61
103
|
lines.push(detail);
|
|
62
104
|
}
|
|
63
105
|
if (items.length > preview.length) {
|
|
@@ -65,6 +107,47 @@ function formatCollection(title, items) {
|
|
|
65
107
|
}
|
|
66
108
|
return lines;
|
|
67
109
|
}
|
|
110
|
+
function formatNestedInventory(data) {
|
|
111
|
+
const lines = [];
|
|
112
|
+
if (isRecord(data.nearbyStores)) {
|
|
113
|
+
lines.push(...formatCollection('주변 매장', data.nearbyStores.stores));
|
|
114
|
+
}
|
|
115
|
+
if (isRecord(data.inventory)) {
|
|
116
|
+
lines.push(...formatCollection('재고 매장', data.inventory.stores));
|
|
117
|
+
lines.push(...formatCollection('재고 항목', data.inventory.items));
|
|
118
|
+
lines.push(...formatCollection('재고 상품', data.inventory.products));
|
|
119
|
+
if (Array.isArray(data.inventory.products)) {
|
|
120
|
+
for (const product of data.inventory.products.slice(0, 3)) {
|
|
121
|
+
if (!isRecord(product) || !isRecord(product.storeInventory)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
lines.push(...formatCollection('상품별 매장 재고', product.storeInventory.stores));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return lines;
|
|
129
|
+
}
|
|
130
|
+
function formatDisplayLocation(data) {
|
|
131
|
+
const lines = [`진열 위치: ${data.hasLocation === true ? '있음' : '없음'}`];
|
|
132
|
+
if (!Array.isArray(data.locations) || data.locations.length === 0) {
|
|
133
|
+
return lines;
|
|
134
|
+
}
|
|
135
|
+
lines.push(`위치 목록: ${data.locations.length}건`);
|
|
136
|
+
for (const location of data.locations.slice(0, 5)) {
|
|
137
|
+
if (!isRecord(location)) {
|
|
138
|
+
lines.push(`- ${toText(location)}`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const zoneNo = toText(location.zoneNo);
|
|
142
|
+
const stairNo = toText(location.stairNo);
|
|
143
|
+
const storeErp = location.storeErp !== undefined ? ` / storeErp ${toText(location.storeErp)}` : '';
|
|
144
|
+
lines.push(`- 구역 ${zoneNo} / 층 ${stairNo}${storeErp}`);
|
|
145
|
+
}
|
|
146
|
+
if (data.locations.length > 5) {
|
|
147
|
+
lines.push(`...외 ${data.locations.length - 5}건`);
|
|
148
|
+
}
|
|
149
|
+
return lines;
|
|
150
|
+
}
|
|
68
151
|
function formatProductDetail(data) {
|
|
69
152
|
const id = data.id ?? data.productId ?? '-';
|
|
70
153
|
const name = data.name ?? data.productName ?? '-';
|
|
@@ -113,14 +196,21 @@ export function renderApiEnvelope(command, url, payload) {
|
|
|
113
196
|
else if (command === 'inventory') {
|
|
114
197
|
lines.push(...formatInventory(data));
|
|
115
198
|
}
|
|
199
|
+
else if (command === 'display-location') {
|
|
200
|
+
lines.push(...formatDisplayLocation(data));
|
|
201
|
+
}
|
|
116
202
|
else if (command === 'cu-inventory') {
|
|
117
203
|
lines.push(...formatCollection('매장 목록', isRecord(data.nearbyStores) ? data.nearbyStores.stores : undefined));
|
|
118
204
|
lines.push(...formatCollection('재고 항목', isRecord(data.inventory) ? data.inventory.items : undefined));
|
|
119
205
|
}
|
|
206
|
+
else if (command.endsWith('-inventory')) {
|
|
207
|
+
lines.push(...formatNestedInventory(data));
|
|
208
|
+
}
|
|
120
209
|
else {
|
|
121
210
|
lines.push(...formatCollection('제품 목록', data.products));
|
|
122
211
|
lines.push(...formatCollection('매장 목록', data.stores));
|
|
123
212
|
lines.push(...formatCollection('극장 목록', data.theaters));
|
|
213
|
+
lines.push(...formatCollection('장소 목록', data.places));
|
|
124
214
|
lines.push(...formatCollection('영화 목록', data.movies));
|
|
125
215
|
lines.push(...formatCollection('시간표', data.timetable));
|
|
126
216
|
lines.push(...formatCollection('좌석 목록', data.seats));
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP/API 공통 에러 진단 구조
|
|
3
|
+
*/
|
|
4
|
+
const SERVICE_PREFIXES = [
|
|
5
|
+
'daiso',
|
|
6
|
+
'gs25',
|
|
7
|
+
'seveneleven',
|
|
8
|
+
'cu',
|
|
9
|
+
'emart24',
|
|
10
|
+
'lottemart',
|
|
11
|
+
'megabox',
|
|
12
|
+
'lottecinema',
|
|
13
|
+
'cgv',
|
|
14
|
+
'oliveyoung',
|
|
15
|
+
];
|
|
16
|
+
function normalizeService(value) {
|
|
17
|
+
return value.toLowerCase().replace(/_/g, '');
|
|
18
|
+
}
|
|
19
|
+
function toOperation(parts) {
|
|
20
|
+
const filtered = parts.filter((part) => part.length > 0 && part !== 'FAILED');
|
|
21
|
+
if (filtered.length === 0) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return filtered.map((part) => part.toLowerCase()).join('_');
|
|
25
|
+
}
|
|
26
|
+
function inferServiceAndOperation(code) {
|
|
27
|
+
const parts = code.split('_').filter((part) => part.length > 0);
|
|
28
|
+
const normalizedFirst = normalizeService(parts[0] || '');
|
|
29
|
+
const service = SERVICE_PREFIXES.find((item) => normalizeService(item) === normalizedFirst);
|
|
30
|
+
if (!service) {
|
|
31
|
+
return { operation: toOperation(parts) };
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
service,
|
|
35
|
+
operation: toOperation(parts.slice(1)),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function isRetryable(code, status) {
|
|
39
|
+
if (typeof status === 'number') {
|
|
40
|
+
return status >= 500 || status === 408 || status === 429;
|
|
41
|
+
}
|
|
42
|
+
const normalized = code.toUpperCase();
|
|
43
|
+
return normalized.includes('TIMEOUT') || normalized.includes('FAILED');
|
|
44
|
+
}
|
|
45
|
+
function buildHint(retryable) {
|
|
46
|
+
return retryable
|
|
47
|
+
? '일시적인 외부 서비스 오류일 수 있습니다. 잠시 후 다시 시도하세요.'
|
|
48
|
+
: '입력값 또는 요청 조건을 확인하세요.';
|
|
49
|
+
}
|
|
50
|
+
export function toStandardErrorDiagnostics(code, message, options = {}) {
|
|
51
|
+
const inferred = inferServiceAndOperation(code);
|
|
52
|
+
const retryable = isRetryable(code, options.status || options.upstreamStatus);
|
|
53
|
+
return {
|
|
54
|
+
code,
|
|
55
|
+
message,
|
|
56
|
+
status: options.status,
|
|
57
|
+
retryable,
|
|
58
|
+
service: options.service || inferred.service,
|
|
59
|
+
operation: options.operation || inferred.operation,
|
|
60
|
+
upstreamStatus: options.upstreamStatus,
|
|
61
|
+
hint: buildHint(retryable),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function getErrorMessage(error) {
|
|
65
|
+
return error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.';
|
|
66
|
+
}
|
package/dist/core/registry.js
CHANGED
|
@@ -3,6 +3,225 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 서비스 프로바이더를 등록하고 MCP 서버에 도구를 연결합니다.
|
|
5
5
|
*/
|
|
6
|
+
import * as z from 'zod';
|
|
7
|
+
import { getErrorMessage, toStandardErrorDiagnostics } from './errors.js';
|
|
8
|
+
const DEFAULT_OUTPUT_SCHEMA = z.object({}).loose().describe('도구 실행 결과(JSON 객체)');
|
|
9
|
+
function createFallbackOutputSchema(toolName) {
|
|
10
|
+
if (toolName.includes('search_products') ||
|
|
11
|
+
toolName.includes('searchProducts') ||
|
|
12
|
+
toolName.endsWith('_products')) {
|
|
13
|
+
return {
|
|
14
|
+
keyword: z.string().optional(),
|
|
15
|
+
query: z.string().optional(),
|
|
16
|
+
page: z.number().optional(),
|
|
17
|
+
pageSize: z.number().optional(),
|
|
18
|
+
totalCount: z.number().optional(),
|
|
19
|
+
count: z.number().optional(),
|
|
20
|
+
products: z.array(z.unknown()).optional(),
|
|
21
|
+
note: z.string().optional(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (toolName.includes('find_stores') ||
|
|
25
|
+
toolName.includes('nearby_stores') ||
|
|
26
|
+
toolName.includes('search_stores')) {
|
|
27
|
+
return {
|
|
28
|
+
keyword: z.string().optional(),
|
|
29
|
+
searchParams: z.unknown().optional(),
|
|
30
|
+
location: z.unknown().optional(),
|
|
31
|
+
totalCount: z.number().optional(),
|
|
32
|
+
filteredCount: z.number().optional(),
|
|
33
|
+
count: z.number().optional(),
|
|
34
|
+
stores: z.array(z.unknown()).optional(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (toolName.includes('places')) {
|
|
38
|
+
return {
|
|
39
|
+
provider: z.string().optional(),
|
|
40
|
+
searchMode: z.string().optional(),
|
|
41
|
+
query: z.string().optional(),
|
|
42
|
+
category: z.string().optional(),
|
|
43
|
+
location: z.string().optional(),
|
|
44
|
+
totalCount: z.number().optional(),
|
|
45
|
+
count: z.number().optional(),
|
|
46
|
+
places: z.array(z.unknown()).optional(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (toolName.includes('inventory')) {
|
|
50
|
+
return {
|
|
51
|
+
productId: z.string().optional(),
|
|
52
|
+
keyword: z.string().optional(),
|
|
53
|
+
itemCode: z.string().nullable().optional(),
|
|
54
|
+
location: z.unknown().optional(),
|
|
55
|
+
product: z.unknown().optional(),
|
|
56
|
+
onlineStock: z.number().optional(),
|
|
57
|
+
inventory: z.unknown().optional(),
|
|
58
|
+
storeInventory: z.unknown().optional(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (toolName.includes('display_location')) {
|
|
62
|
+
return {
|
|
63
|
+
productId: z.string().optional(),
|
|
64
|
+
storeCode: z.string().optional(),
|
|
65
|
+
displayLocation: z.unknown().optional(),
|
|
66
|
+
locations: z.array(z.unknown()).optional(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (toolName.includes('price_info')) {
|
|
70
|
+
return {
|
|
71
|
+
productId: z.string().optional(),
|
|
72
|
+
productName: z.string().optional(),
|
|
73
|
+
product: z.unknown().optional(),
|
|
74
|
+
price: z.number().optional(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (toolName.includes('theater')) {
|
|
78
|
+
return {
|
|
79
|
+
keyword: z.string().optional(),
|
|
80
|
+
location: z.unknown().optional(),
|
|
81
|
+
count: z.number().optional(),
|
|
82
|
+
theaters: z.array(z.unknown()).optional(),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (toolName.includes('movie') || toolName.includes('now_showing')) {
|
|
86
|
+
return {
|
|
87
|
+
keyword: z.string().optional(),
|
|
88
|
+
playDate: z.string().optional(),
|
|
89
|
+
theaterCode: z.string().optional(),
|
|
90
|
+
count: z.number().optional(),
|
|
91
|
+
movies: z.array(z.unknown()).optional(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (toolName.includes('timetable') || toolName.includes('remaining_seats')) {
|
|
95
|
+
return {
|
|
96
|
+
playDate: z.string().optional(),
|
|
97
|
+
theaterCode: z.string().optional(),
|
|
98
|
+
movieCode: z.string().optional(),
|
|
99
|
+
count: z.number().optional(),
|
|
100
|
+
showtimes: z.array(z.unknown()).optional(),
|
|
101
|
+
seats: z.array(z.unknown()).optional(),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (toolName.includes('popwords')) {
|
|
105
|
+
return {
|
|
106
|
+
label: z.string().optional(),
|
|
107
|
+
keywords: z.array(z.string()).optional(),
|
|
108
|
+
count: z.number().optional(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (toolName.includes('catalog')) {
|
|
112
|
+
return {
|
|
113
|
+
pages: z.array(z.unknown()).optional(),
|
|
114
|
+
issues: z.array(z.unknown()).optional(),
|
|
115
|
+
exhibitions: z.array(z.unknown()).optional(),
|
|
116
|
+
count: z.number().optional(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
function createToolOutputSchema(toolName, outputSchema) {
|
|
122
|
+
const schema = outputSchema || createFallbackOutputSchema(toolName);
|
|
123
|
+
if (Object.keys(schema).length === 0) {
|
|
124
|
+
return DEFAULT_OUTPUT_SCHEMA;
|
|
125
|
+
}
|
|
126
|
+
return z
|
|
127
|
+
.object({
|
|
128
|
+
...schema,
|
|
129
|
+
standard: z.unknown().optional().describe('서비스 공통 상품/매장/영화관 정규화 결과'),
|
|
130
|
+
})
|
|
131
|
+
.loose();
|
|
132
|
+
}
|
|
133
|
+
function parseJsonText(text) {
|
|
134
|
+
try {
|
|
135
|
+
const parsed = JSON.parse(text);
|
|
136
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
function firstString(...values) {
|
|
146
|
+
for (const value of values) {
|
|
147
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
148
|
+
return value.trim();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
function firstNumber(...values) {
|
|
154
|
+
for (const value of values) {
|
|
155
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
if (typeof value === 'string') {
|
|
159
|
+
const parsed = Number.parseFloat(value.replace(/km$/i, ''));
|
|
160
|
+
if (Number.isFinite(parsed)) {
|
|
161
|
+
return value.trim().toLowerCase().endsWith('km') ? Math.round(parsed * 1000) : parsed;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
function normalizeProducts(products) {
|
|
168
|
+
return products
|
|
169
|
+
.filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item))
|
|
170
|
+
.map((item) => ({
|
|
171
|
+
code: firstString(item.itemCode, item.productCode, item.pluCd, item.id, item.productNo),
|
|
172
|
+
name: firstString(item.itemName, item.shortItemName, item.goodsName, item.productName, item.movieName, item.name),
|
|
173
|
+
price: firstNumber(item.viewPrice, item.salePrice, item.sellPrice, item.price, item.originPrice, item.searchItemSellPrice),
|
|
174
|
+
imageUrl: firstString(item.imageUrl),
|
|
175
|
+
raw: item,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
function normalizeStores(stores) {
|
|
179
|
+
return stores
|
|
180
|
+
.filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item))
|
|
181
|
+
.map((item) => ({
|
|
182
|
+
code: firstString(item.storeCode, item.code, item.id),
|
|
183
|
+
name: firstString(item.storeName, item.name, item.theaterName),
|
|
184
|
+
address: firstString(item.address, item.storeAddress),
|
|
185
|
+
distanceMeters: firstNumber(item.distanceM, item.distanceMeters, item.distance),
|
|
186
|
+
raw: item,
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
function withStandardCollections(payload) {
|
|
190
|
+
const standard = {};
|
|
191
|
+
if (Array.isArray(payload.products)) {
|
|
192
|
+
standard.products = normalizeProducts(payload.products);
|
|
193
|
+
}
|
|
194
|
+
if (Array.isArray(payload.stores)) {
|
|
195
|
+
standard.stores = normalizeStores(payload.stores);
|
|
196
|
+
}
|
|
197
|
+
if (Array.isArray(payload.places)) {
|
|
198
|
+
standard.places = normalizeStores(payload.places);
|
|
199
|
+
}
|
|
200
|
+
if (Array.isArray(payload.theaters)) {
|
|
201
|
+
standard.theaters = normalizeStores(payload.theaters);
|
|
202
|
+
}
|
|
203
|
+
if (Array.isArray(payload.movies)) {
|
|
204
|
+
standard.movies = normalizeProducts(payload.movies);
|
|
205
|
+
}
|
|
206
|
+
if (Object.keys(standard).length === 0) {
|
|
207
|
+
return payload;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
...payload,
|
|
211
|
+
standard,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function buildStructuredContent(result) {
|
|
215
|
+
if (result.structuredContent) {
|
|
216
|
+
return withStandardCollections(result.structuredContent);
|
|
217
|
+
}
|
|
218
|
+
const text = result.content[0]?.text || '';
|
|
219
|
+
const parsed = parseJsonText(text);
|
|
220
|
+
if (parsed) {
|
|
221
|
+
return withStandardCollections(parsed);
|
|
222
|
+
}
|
|
223
|
+
return { text };
|
|
224
|
+
}
|
|
6
225
|
/**
|
|
7
226
|
* 서비스 레지스트리 클래스
|
|
8
227
|
*
|
|
@@ -73,14 +292,38 @@ export class ServiceRegistry {
|
|
|
73
292
|
* 단일 도구를 MCP 서버에 등록
|
|
74
293
|
*/
|
|
75
294
|
registerTool(server, tool) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
295
|
+
const metadata = {
|
|
296
|
+
...tool.metadata,
|
|
297
|
+
outputSchema: createToolOutputSchema(tool.name, tool.metadata.outputSchema),
|
|
298
|
+
};
|
|
299
|
+
server.registerTool(tool.name, metadata, async (args) => {
|
|
300
|
+
let result;
|
|
301
|
+
try {
|
|
302
|
+
result = await tool.handler(args);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
const message = getErrorMessage(error);
|
|
306
|
+
return {
|
|
307
|
+
isError: true,
|
|
308
|
+
content: [{ type: 'text', text: message }],
|
|
309
|
+
structuredContent: {
|
|
310
|
+
error: toStandardErrorDiagnostics('TOOL_EXECUTION_FAILED', message, {
|
|
311
|
+
operation: tool.name,
|
|
312
|
+
}),
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const response = {
|
|
79
317
|
content: result.content.map((item) => ({
|
|
80
318
|
type: item.type,
|
|
81
319
|
text: item.text,
|
|
82
320
|
})),
|
|
321
|
+
structuredContent: buildStructuredContent(result),
|
|
83
322
|
};
|
|
323
|
+
if (result.isError) {
|
|
324
|
+
response.isError = true;
|
|
325
|
+
}
|
|
326
|
+
return response;
|
|
84
327
|
});
|
|
85
328
|
}
|
|
86
329
|
/**
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { createCuService } from './services/cu/index.js';
|
|
|
19
19
|
import { createEmart24Service } from './services/emart24/index.js';
|
|
20
20
|
import { createGs25Service } from './services/gs25/index.js';
|
|
21
21
|
import { createSevenElevenService } from './services/seveneleven/index.js';
|
|
22
|
+
import { createPlacesService } from './services/places/index.js';
|
|
22
23
|
import { createPromptResponse } from './pages/prompt.js';
|
|
23
24
|
import { createFullOpenApiJsonResponse, createFullOpenApiYamlResponse, createOpenApiJsonResponse, createOpenApiYamlResponse, } from './pages/openapi.js';
|
|
24
25
|
import { createPrivacyResponse } from './pages/privacy.js';
|
|
@@ -33,7 +34,9 @@ import { registerCuRoutes } from './api/routes/cuRoutes.js';
|
|
|
33
34
|
import { registerEmart24Routes } from './api/routes/emart24Routes.js';
|
|
34
35
|
import { registerGs25Routes } from './api/routes/gs25Routes.js';
|
|
35
36
|
import { registerSevenElevenRoutes } from './api/routes/sevenelevenRoutes.js';
|
|
37
|
+
import { registerPlacesRoutes } from './api/routes/placesRoutes.js';
|
|
36
38
|
import { registerHealthRoutes } from './api/routes/healthRoutes.js';
|
|
39
|
+
import { buildConfigStatus } from './api/configStatus.js';
|
|
37
40
|
// 서버 메타데이터
|
|
38
41
|
const SERVER_NAME = 'multi-service-mcp';
|
|
39
42
|
const SERVER_VERSION = '1.0.0';
|
|
@@ -47,8 +50,15 @@ const createRegistry = (bindings) => {
|
|
|
47
50
|
const registry = new ServiceRegistry();
|
|
48
51
|
registry.registerAll([
|
|
49
52
|
createDaisoService,
|
|
50
|
-
createGs25Service
|
|
53
|
+
() => createGs25Service({
|
|
54
|
+
googleMapsApiKey: bindings?.GOOGLE_MAPS_API_KEY,
|
|
55
|
+
zyteApiKey: bindings?.ZYTE_API_KEY,
|
|
56
|
+
}),
|
|
51
57
|
createSevenElevenService,
|
|
58
|
+
() => createPlacesService({
|
|
59
|
+
naverClientId: bindings?.NAVER_CLIENT_ID,
|
|
60
|
+
naverClientSecret: bindings?.NAVER_CLIENT_SECRET,
|
|
61
|
+
}),
|
|
52
62
|
createCuService,
|
|
53
63
|
createEmart24Service,
|
|
54
64
|
() => createLotteMartService({
|
|
@@ -86,9 +96,13 @@ const createMcpServer = (bindings) => {
|
|
|
86
96
|
*/
|
|
87
97
|
const isInitializeRequest = (body) => {
|
|
88
98
|
if (Array.isArray(body)) {
|
|
89
|
-
return body.some((item) => typeof item === 'object' &&
|
|
99
|
+
return body.some((item) => typeof item === 'object' &&
|
|
100
|
+
item !== null &&
|
|
101
|
+
item.method === 'initialize');
|
|
90
102
|
}
|
|
91
|
-
return typeof body === 'object' &&
|
|
103
|
+
return (typeof body === 'object' &&
|
|
104
|
+
body !== null &&
|
|
105
|
+
body.method === 'initialize');
|
|
92
106
|
};
|
|
93
107
|
/**
|
|
94
108
|
* 새 세션 transport/server를 생성합니다.
|
|
@@ -196,7 +210,7 @@ app.on(['POST', 'DELETE', 'OPTIONS'], '/', async (c) => {
|
|
|
196
210
|
return transport.handleRequest(c.req.raw);
|
|
197
211
|
});
|
|
198
212
|
// 헬스 체크 엔드포인트
|
|
199
|
-
app.get('/health', (c) => c.json({ status: 'ok' }));
|
|
213
|
+
app.get('/health', (c) => c.json({ status: 'ok', config: buildConfigStatus(c.env) }));
|
|
200
214
|
registerHealthRoutes(app);
|
|
201
215
|
// 프롬프트 페이지 (MCP 미지원 에이전트용)
|
|
202
216
|
app.get('/prompt', (c) => {
|
|
@@ -229,6 +243,7 @@ app.get('/privacy', (c) => {
|
|
|
229
243
|
registerDaisoRoutes(app);
|
|
230
244
|
registerGs25Routes(app);
|
|
231
245
|
registerSevenElevenRoutes(app);
|
|
246
|
+
registerPlacesRoutes(app);
|
|
232
247
|
registerCuRoutes(app);
|
|
233
248
|
registerEmart24Routes(app);
|
|
234
249
|
registerLotteMartRoutes(app);
|
|
@@ -10,6 +10,7 @@ import { OPENAPI_PATHS_CGV } from './openapiSpecPathsCgv.js';
|
|
|
10
10
|
import { OPENAPI_PATHS_LOTTECINEMA } from './openapiSpecPathsLottecinema.js';
|
|
11
11
|
import { OPENAPI_PATHS_GS25 } from './openapiSpecPathsGs25.js';
|
|
12
12
|
import { OPENAPI_PATHS_SEVENELEVEN } from './openapiSpecPathsSeveneleven.js';
|
|
13
|
+
import { OPENAPI_PATHS_PLACES } from './openapiSpecPathsPlaces.js';
|
|
13
14
|
import { OPENAPI_COMPONENTS } from './openapiSpecComponents.js';
|
|
14
15
|
export function generateFullOpenApiSpec(baseUrl) {
|
|
15
16
|
return {
|
|
@@ -29,6 +30,7 @@ export function generateFullOpenApiSpec(baseUrl) {
|
|
|
29
30
|
- 🎬 **메가박스 지점/영화 조회**: 주변 지점, 상영 목록, 잔여 좌석 조회
|
|
30
31
|
- 🎞️ **롯데시네마 지점/영화 조회**: 주변 지점, 상영 목록, 잔여 좌석 조회
|
|
31
32
|
- 🎥 **CGV 지점/영화 조회**: 극장 목록, 영화 목록, 시간표 조회
|
|
33
|
+
- 🍽️ **주변 음식점/카페 검색**: 네이버 지역 검색 기반 장소 조회
|
|
32
34
|
|
|
33
35
|
## 사용 팁
|
|
34
36
|
1. 한글 검색어는 URL 인코딩이 자동 처리됩니다
|
|
@@ -49,6 +51,7 @@ export function generateFullOpenApiSpec(baseUrl) {
|
|
|
49
51
|
...OPENAPI_PATHS_LOTTEMART,
|
|
50
52
|
...OPENAPI_PATHS_GS25,
|
|
51
53
|
...OPENAPI_PATHS_SEVENELEVEN,
|
|
54
|
+
...OPENAPI_PATHS_PLACES,
|
|
52
55
|
...OPENAPI_PATHS_MEGABOX,
|
|
53
56
|
...OPENAPI_PATHS_LOTTECINEMA,
|
|
54
57
|
...OPENAPI_PATHS_CGV,
|